Java抽象类和接口
抽象类
我们学习了面向对象以及面向对象的特征,我们知道父类一般描述的是一类事物的共性,也就是说父类就是对基类的初步抽象,但是有的时候我们可能只知道子类具有行为,但是不能确切的知道这些子类如何实现这些方法,例如,我们都知道绝大多数鸟都会飞,但是每种鸟飞的高度和速度都不同,这样父类也就不能准确的描述子类飞行这个行为。即便是父类定义了方法,不同的鸟根据实际情况也会重写这个方法,那么这个方法的定义也就显得多余。
你可能会想到,能不能只定义方法头,而不提供方法的具体实现,让这个类的子类去自己实现呢?在Java中,这种行为是允许的,这就是我们我们本章节学习的抽象类和接口。
抽象类和抽象方法
抽象类就是使用abstract修饰的类。抽象方法就是使用abstract修饰的方法。抽象类中可以包含抽象方法,也可以不包含。
抽象类和抽象方法的规则如下:
- 抽象类必须使用 abstract 修饰符来修饰,抽象方法也必须使用 abstract 修饰符来修饰,抽象方法 不能有方法体。
- 抽象类不能被实例化,无法使用 new 关键字来调用抽象类的构造器创建抽象类的实例。即使抽 象类里不包含抽象方法,这个抽象类也不能创建实例。
- 抽象类可以包含成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接 口、枚举)5种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。
- 含有抽象方法的类(包括直接定义了一个抽象方法;或继承了一个抽象父类,但没有完全实现 父类包含的抽象方法;或实现了一个接口,但没有完全实现接口包含的抽象方法三种情况)只能被定义成抽象类。
定义抽象类只需在普通类上增加 abstract 修饰符即可。甚至一个普通类(没有包含抽象方法的类)增加 abstract 修饰符后也将变成抽象类。
1 | package chapter07; |
在上面的示例中,定义了一个抽象类,抽象类的命名一般用Abstract作为前缀,在非抽象类中定义的内容在抽象类中同样可以定义,并且抽象类中还可以定义抽象方法,在代码16行,定义了一个抽象方法,从示例中可以看出,抽象方法使用了abstract修饰,并且抽象方法只有方法头,没有方法体。参数列表后直接以“;”结束。
下面定义子类并重写父类的方法:
1 | package chapter07; |
上例中继承了抽象类AbstractCalc,并重写了父类的抽象方法,因为父类方法使用了可变参数,而计算圆面积时,只需要传入半径即可,因此先判断可变参数是否为null以及长度是否为0,并且只有1个参数时,就可以确定是在计算圆面积。需要注意的是:
- 子类继承抽象类必须重写父类的抽象方法(普通实例方法可以不重写),如果不重写父类的抽象方法,那么自己也必须是一个抽象类。
- abstract只能修饰类和方法,不能修饰成员变量和局部变量、构造器等,没有抽象变量和抽象构造方法。
- 抽象类也是单继承的,不管是普通类继承抽象类还是抽象类继承抽象类,都是单继承的。
除此之外,当使用 static 修饰一个方法时,表明这个方法属于该类本身,即通过类就可调用该方法,但如果该方法被定义成抽象方法,则将导致通过该类来调用该方法时出现错误(调用了一个没有方法体的方法肯定会引起错误)。因此static 和 abstract 不能同时修饰某个方法,即没有所谓的类抽象方法。
abstract 关键字修饰的方法必须被其子类重写才有意义,否则这个方法将永远不会有方法体,因此 abstract 方法不能定义为 private 访问权限,即 private和abstract 不能同时修饰方法。
从前面的示例程序可以看出,抽象类不能创建实例,只能当成父类来被继承。从语义的角度来看,抽象类是从多个具体类中抽象出来的父类,它具有更高层次的抽象。·从多个具有相同特征的类中抽象出一个抽象类,以这个抽象类作为其子类的模板,从而避免了子类设计的随意性。 抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会大致保留抽象类的行为方式。 如果编写一个抽象父类,父类提供了多个子类的通用方法,并把一个或多个方法留给其子类实现,这就是一种模板模式,模板模式也是十分常见且简单的设计模式之一。例如前面介绍的 Shape、Circle和Triangle 三个类,已经使用了模板模式。
接口
抽象类是从多个类中抽象出来的模板,如果将这种抽象更彻底一些,可以抽象出更加简洁和精炼的类——接口。
在日常生活中,我们经常可以听到某某接口,例如:USB接口、打印机接口等,你可能会将Java中的接口和上述的这些接口等同,其实这是不准确的,在Java中的接口更倾向于描述行为规范,也就等价于描述的一些规定。例如经常听到一句话叫做一流公司做标准,这里的标准就可以理解为Java中的接口,不同的工厂生产的产品遵守这个标准和规范产品才能在市场上流通。因此,Java中的接口可以理解为定义了一种规范,接口定义了某一批类所要遵守的规范,接口并不关心类的内部状态数据,也不关注这些方法里的实现细节,它只规定子类里必须提供某些方法。
由此可见,接口是从多个相似的类中抽象出来的规范,接口不提供任何实现。
接口的定义
定义接口使用关键字interface,接口的定义语法如下:
1 | [修饰符] interface 接口名 extends 父接口1,父接口2,父接口3...{ |
对于上面语法说明如下:
- 修饰符可以是 public 或者省略,如果省略了public 访问控制符,则默认采用包权限访问控制符, 即只有在相同包结构下才可以访问该接口。
- 接口名应与类名采用相同的命名规则,即如果仅从语法角度来看,接口名只要是合法的标识符 即可;如果要遵守 Java 可读性规范,则接口名应由多个有意义的单词连缀而成,每个单词首字母大写,单词与单词之间无须任何分隔符。接口名通常能够使用形容词。
- 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口里可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、内部类(包括内部接口、枚举)定义。
对比接口和类的定义方式,不难发现接口的成员比类里的成员少了两种(构造器和初始化块)而且接口里的成员变量只能是静态常量,接口里的方法只能是抽象方法、类方法、默认方法或私有方法。
前面已经说过了,接口里定义的是多个类共同的公共行为规范,因此接口里的常量、方法、内部类和内部枚举都是 public 访问权限。定义接口成员时,可以省略访问控制修饰符,如果指定访问控制修饰符,则只能使用 public 访问控制修饰符。
Java9为接口增加了一种新的私有方法,其实私有方法的主要作用就是作为工具方法,为接口中的默认方法或类方法提供支持。私有方法可以拥有方法体,但私有方法不能使用 default 修饰。私有方法可以使用 static 修饰,也就是说,私有方法既可是类方法,也可是实例方法。
对于接口里定义的静态常量而言,它们是接口相关的,因此系统会自动为这些成员变量增加 static和 final 两个修饰符。也就是说,在接口中定义成员变量时,不管是否使用 public static final修饰符,接口里的成员变量总是使用这三个修饰符来修饰。而且接口里没有构造器和初始化块,因此接口里定义的成员变量只能在定义时指定默认值。
接口中定义的变量都是常量,默认的修饰符就是public static final ,因此即使不加,在编译时编译器也会自动添加
1 | int MAX_VALUE = 100; |
接口里定义的方法只能是抽象方法、类方法、默认方法或私有方法,因此如果不是定义默认方法、类方法或私有方法,系统将自动为普通方法增加 abstract 修饰符;定义接口里的普通方法时不管是否使用 public abstract 修饰符,接口里的普通方法总是使用public abstract 来修饰。接口里的普通方法不能有方法实现(方法体); 但类方法、默认方法、私有方法都必须有方法实现(方法体)。
下面,通过示例来学习接口的定义:
1 | package chapter07; |
需要注意的是子类继承了接口只有抽象方法是需要强制重写的,否则子类就需要使用abstract修饰。
Java 8 允许在接口中定义默认方法,默认方法必须使用 default 修饰,该方法不能使用 static 修饰,无论程序是否指定,默认方法总是使用 public 修饰——如果开发者没有指定 public,系统会自动为默认方法添加 public 修饰符。由于默认方法并没有 static 修饰,因此不能直接使用接口来调用默认方法,需要使用接口的实现类的实例来调用这些默认方法。
Java8允许在接口中定义类方法,类方法必须使用 static 修饰,该方法不能使用 default 修饰,无论程序是否指定,类方法总是使用 public 修饰——如果开发者没有指定 public,系统会自动为类方法添加 public 修饰符。类方法可以直接使用接口来调用。
Java9增加了带方法体的私有方法。
下面定义一个接口的实现类:
1 | package chapter07; |
从上例中我们可以得出接口中的抽象方法子类必须重写,否则就需要用abstract修饰,默认方法则可以选择性重写,而私有方法则不需要重写。
静态方法可以被继承,但是,不能被覆盖,即重写。如果父类中定义的静态方法在子类中被重新定义,那么在父类中定义的静态方法将被隐藏。可以使用语法:父类名.静态方法调用隐藏的静态方法。 如果父类中含有一个静态方法,且在子类中也含有一个返回类型、方法名、参数列表均与之相同的静态方法,那么该子类实际上只是将父类中的该同名方法进行了隐藏,而非重写。换句话说,父类和子类中含有的其实是两个没有关系的方法,它们的行为也并不具有多态性 因此,通过一个指向子类对象的父类引用变量来调用父子同名的静态方法时,只会调用父类的静态方法。
Java中的静态方法不能被子类重写
特点:静态方法属于类的方法,也属于对象的方法,但是静态方法随类的存在。
结论:Java中的静态方法不能被子类重写,实际上,子类的静态方法隐藏了父类的静态方法,因此父类的子类的静态方法同时存在,只不过父类通过类名(或对象名)调用的是父类的静态方法,子类通过类名(或对象名)调用的是子类的
结论:
(1)静态方法不支持多态。(final,private 方法也如此)
(2)静态方法可通过类名直接调用,也可通过类的实例化对象调用,因此Father 的实例化对象f1调用的是父类(不是子类)的静态方法。
(3)静态方法的绑定时期为代码的编译器期,也叫前期绑定。非静态的方法属于程序的执行期绑定,也就运行期绑定。
接口和抽象类的区别1
(还要说接口、抽象类都包含什么)
抽象类:抽象类可以包含成员变量、方法(普通方法(其它方法)和抽象方法都可以)、构造器、初始化块、内部类(接 口、枚举)5种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。
接口:接口里可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、内部类(包括内部接口、枚举)定义。
从某种程度上来看,接口类似于整个系统的”总纲”,它制定了系统各模块应该遵循的标准,因此一个系统中的接口不应该经常改变。一旦接口被改变,对整个系统甚至其他系统的影响将是辐射式的,导致系统中大部分类都需要改写。
抽象类则不一样,抽象类作为系统中多个子类的共同父类,它所体现的是一种模板式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能(那些已经提供实现的方法),但这个产品依然不能当成最终产品,必须有更进一步的完善,这种完善可能有几种不同方式。
除此之外,接口和抽象类在用法上也存在如下差别。
接口和抽象类很像,它们都具有如下特征。
接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。
接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现; 抽象类则完全可以包含普通方法。
接口里只能定义静态常量,不能定义普通成员变量;抽象类里则既可以定义普通成员变量,也 可以定义静态常量。
接口里不包含构造器; 抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而 是让其子类调用这些构造器来完成属于抽象类的初始化操作。
接口里不能包含初始化块; 但抽象类则完全可以包含初始化块。
一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多 个接口可以弥补Java 单继承的不足。
接口和抽象类的区别2
编译时类型由声明该变量时的类型决定,运行时类型由赋值给该变量的对象类型决定,在继承关系中,当编译时类型和运行时类型不一致时,子类重写父类方法,编译时类型调用相同方法时,总是表现出子类方法的特性,此时就产生了多态(父类引用指向子类对象)
注意
多态发生在继承关系当中
对象的实例变量不具备多态性
抽象类和接口的区别:
相同:
(1)抽象类和接口都不能被实例化,他们位于继承树的顶端。
(2)抽象类和接口都可以包含抽象方法,实现接口或继承抽象类的普通类必须实现这些抽象方法。
不同:
(1)抽象类包含类变量,实例变量,方法( 普通方法,抽象方法,默认方法,静态方法,私有方法 ),构造方法,初始化块、内部类(接口,枚举)。接口只能包含静态常量,抽象方法,默认方法(JAVA 8),私有方法(JAVA 9),类方法,内部类(接口,枚举)。
(2)接口不含初始化块和构造方法,抽象类包含,但是抽象类的构造器不是用来创建对象的,而是用来被普通子类调用来初始化抽象类。
(3)在Java中,抽象类本质上也是一个类,所以抽象类是单继承的,一个类只能有一个直接父类,为了弥补java单继承的不足,接口与接口之间可以进行多继承,一个类可以实现多个接口,一个接口可以继承多个接口,接口只能继承接口。
内部类
在类内部定义的类就叫做内部类,包含内部类的类也被叫做外部类或者宿主类,Java从JDK1.1 开始引入了内部类,目的是为Java提供了更好的封装。内部类具有以下作用:
- 内部类隐藏在外部类中,不允许同一个包中的其他类访问
- 内部类成员可以直接访问外部类的私有数据,内部类被当成其外部类成员,同一个类的成员之间可以相互访问。但外部类不能访问内部类的实现细节,例如内部类的成员变量。
从语法角度来看,定义内部类与定义外部类的语法大致相同,内部类除了需要定义在外部类内之外,还有以下两点区别:
- 内部类比外部类多三个修饰符:private、protected、static
- 非静态内部不能拥有静态成员
1 | public class OuterClass{ |
对于java类加载顺序我们知道,首先加载类,执行static变量初始化,接下来执行对象的创建,如果我们要执行代码中的变量i初始化,
那么必须先执行加载OuterClass,再加载Innerclass,最后初始化静态变量i,问题就出在加载Innerclass上面,我们可以把InnerClass看成OuterClass的非静态成员,它的初始化必须在外部类对象创建后以后进行,要加载InnerClass必须在实例化OuterClass之后完成 ,java虚拟机要求所有的静态变量必须在对象创建之前完成,这样便产生了矛盾。
非静态内部类的创建、访问都需要有一个外部类的实例,通过外部类的实例才能访问到内部类。从底层的角度来说,外部类的实例持有指向内部类的指针,只有通过外部类实例才能访问到内部类的数据。 那么,为什么非静态内部类不能有静态的成员呢! 首先从内存分配角度来说,众所周知,静态成员是在类加载时候分配内存空间的;但对于内部类来说,要访问它的成员,就要有一个外部类实例,但是在加载类的时候不可能实例化一个外部类给内部类的,因此,没有任何外部类的实例持有这个静态成员的指针,内部类的静态成员是无法访问到的,所以Java不允许有非静态内部类的静态成员。
非静态内部类
定义内部类并没有什么特殊之处,只需要把一个类定义在另一个类内部即可。下面通过示例来定义内部类:
1 | package cn.bytecollege.inner; |
从上面的示例可以看出,内部类位于外部类的大括号中,如果位于外部类的大括号以外,则不能叫做内部类,只是源文件中定义了两个Java类而已(一个源文件中只能有一个public修饰的类,并且类名要和文件名相同)。
内部类可以看做类的成员,成员内部类又分为静态内部类和非静态内部类,成员内部类和成员变量、成员方法、初始化块等一样,都是类的成员。下面,通过示例继续学习非静态内部类。
1 | package cn.bytecollege.inner; |
在上例38行创建了内部类对象,因为非静态内部类和实例变量一样,都依赖于对象存在,因此必须先创建外部类以后才能继续创建内部类。并且我们在内部类实例方法中访问了外部类的私有成员变量。创建完内部类对象后调用方法和普通类的对象调用方法并没有什么区别。
需要注意的是:非静态内部类的成员可以访问外部类的 private 成员,但反过来就不成立了。非静态内部类的成员在非静态内部类范围内是可知的,并不能被外部类直接使用。如果外部类需要访问内部类的成员,则必须显式创建非静态内部类对象来调用访问其实例成员。
1 | package cn.bytecollege.inner; |
上例中,代码15行在外部类成员方法中访问了内部类的成员,发现编译出错,因为外部类不能直接访问内部类的属性。
另外需要注意的是:非静态内部类里不能有静态方法、静态成员变量、静态初始化块,但是可以包含初始化块。
静态内部类
当内部类被static修饰后,就成为了静态内部类,和类变量、类方法、静态代码块一样具有同等的地位。static 关键字的作用是把类的成员变成类相关,而不是实例相关,即 static 修饰的成员属于整个类,而不属于单个对象。
静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。即使是静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员。下面我们通过示例来验证这条结论
1 | package cn.bytecollege.inner; |
通过上例14行可以看出静态内部类只能访问外部类的静态成员,不能访问实例成员。
那么静态内部类又该如何创建对象呢,下面通过示例来学习:
1 | package cn.bytecollege.inner; |
局部内部类
如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。由于局部内部类不能在外部类的方法以外的地方使用,因此局部内部类也不能使用访问控制符和 static 修饰符修饰。
1 | package cn.bytecollege.inner; |
匿名内部类
匿名内部类在Java web开发中使用的比较少,但是在Android开发中经常使用,匿名内部类的定义语法如下:
1 | new 实现接口() | 父类构造器(参数列表){ |
从上面定义可以看出,匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类,或实现一个接口。 关于匿名内部类还有如下两条规则。
- 匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。因 此不允许将匿名内部类定义成抽象类。
- 匿名内部类不能定义构造器。由于匿名内部类没有类名,所以无法定义构造器,但匿名内部类 可以定义初始化块,可以通过实例初始化块来完成构造器需要完成的事情。
1 | package cn.bytecollege.inner; |
在示例中,AnonDemo中定义了方法test(),这个方法的参数是一个接口。因为接口不能被实例化,所以不能直接创建对象,因此创建了一个实现类的对象传入了方法。并且从示例中可以发现定义匿名内部类不需要关键字class,在定义匿名内部类时直接生成该匿名内部类的对象。
如果把接口更换成抽象类,定义匿名内部类的方法和上例相同,只是创建的类变成了抽象类的子类对象,并且内部类也要重新抽象类中定义的抽象方法。
枚举类
在特定情况下,一个类的对象是有限且固定的,例如季节、星期、性别等,这种实例有限且固定的类,Java中称为枚举类。
在引入枚举之前,这种情况一般使用常量代替。但是这么做有一定的缺陷,例如输入性别时,一般只输出1或者2很难去猜测其代表的含义,因此也降低了代码的可读性。
Java5新增了一个enum关键字,用于定义枚举类(枚举也可以看做是一个特殊的类)。枚举同样可以拥有自己的成员变量和方法,也可是实现一个或者多个接口,也可以定义自己的构造方法。和定义类一样,一个枚举源文件中最多只能定义一个public访问权限的枚举类,并且该源文件名称也必须和枚举类相同。
枚举类和普通类存在以下差异:
- 枚举类可以实现一个或多个接口,使用 enum 定义的枚举类默认继承了java.lang.Enum 类,而不是默认继承 Object 类,因此枚举类不能显式继承其他父类。其中 java.lang.Enum 类实现. java.lang.Serializable 和 java.lang. Comparable 两个接口。
- 使用 enum 定义、非抽象的枚举类默认会使用 final 修饰,因此枚举类不能派生子类。
- 枚举类的构造器只能使用 private 访问控制符, 如果省略了构造器的访问控制符。则默认使用 private 修饰; 如果强制指定访问控制符,则只能指定 private 修饰符。
- 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例。列出这些实例时,系统会自动添加 public static final 修饰,无须程序员显式添加。
定义枚举类
下面,我们定义一个简单的性别枚举
1 | package cn.bytecollege; |
定义枚举类时,需要显式列出所有的枚举值。所有的枚举值之间用“,”隔开。枚举值结束后用英文分号作为结束。
在第3章switch一节中知道switch支持了枚举类型,下面通过示例学习枚举在switch中的使用:
1 | package cn.bytecollege; |
上面的示例测试了枚举的的基本用法以及在switch分支结构中的使用。
因为枚举都继承了java.lang.Enum类,所以枚举类可以使用Enum类中的方法,下面介绍3个重要的方法:
- String name()∶ 返回此枚举实例的名称,这个名称就是定义枚举类时列出的所有枚举值之一。与 此方法相比,大多数程序员应该优先考虑使用 toString(O方法,因为 toStringO)方法返回更加用户友好的名称。
- int ordinal()∶返回枚举值在枚举类中的索引值(就是枚举值在枚举声明中的位置,第一个枚举 值的索引值为零)。
- String toString()∶返回枚举常量的名称,与name 方法相似,但 toStringO方法更常用。
1 | package cn.bytecollege; |
当程序打印枚举值时,实际上输出的是该枚举值的 toString()方法,也就是输出该枚举值的名字。
枚举类的构造方法
枚举类也是一种类,只是它是一种比较特殊的类,因此它一样可以定义成员变量、方法和构造器。
1 | package cn.bytecollege; |
接着,在枚举中定义构造方法:
1 | package cn.bytecollege; |
从上面程序中可以看出,当为Gender 枚举类创建了一个Gender(String name)构造器之后,列出枚举值就应该采用粗体字代码来完成。也就是说,在枚举类中列出枚举值时,实际上就是调用构造器创建枚举类对象,只是这里无须使用 new 关键字,也无需显式调用构造器。前面列出枚举值时无需传入参数,甚至无需使用括号,仅仅是因为前面的枚举类包含无参数的构造器。
枚举中的抽象方法
假设有一个 Operation 枚举类,它的 4个枚举值 PLUS,MINUS,TIMES,DIVIDE 分别代表加、减、乘、除 4 种运算,该枚举类需要定义一个eval()方法来完成计算。 从上面描述可以看出,Operation 需要让PLUS、MINUS、TIMES、DIVIDE 四个值对 evalO方法各有不同的实现。此时可考虑为 Operation 枚举类定义一个eval()抽象方法,然后让4个枚举值分别为eval()提供不同的实现。例如如下代码。
1 | package cn.bytecollege; |
编写测试类:
1 | package cn.bytecollege; |
注意:枚举类里定义抽象方法时不能使用 abstract 关键字将枚举类定义成抽象类(因为系统自动会为它添加 abstract 关铰字),但因为枚举类需要显式创建枚举值,而不是作为父类,所以定义每个枚举值时必须为抽象方法提供实现,否则将出现编译错误。