Java 抽象类与接口

1. 抽象类
在 多态 文中提到了 抽象 的概念:如果一个类定义了没有具体执行代码的方法(即只有定义,没有实现),这个方法就是抽象方法,并用 abstract 修饰(不能定义为 final)。因为无法执行抽象方法,因此这个类也必须定义为抽象类,但是抽象类中可以不定义抽象方法。
在阿里巴巴 Java 开发手册中抽象类的类名必须带有 Abstract 的开头。
抽象类本身被设计成 只能用于被继承,并且在其子类中 必须覆写 抽象类中的 抽象方法。
之前文中的多态实现代码中,就带有一个如下的抽象类:
|
|
其中 sound() 方法即为所谓的抽象方法,在任何继承该抽象类的子类中 sound() 方法必须覆写,如果在抽象类中实现了普通的方法,则无须覆写直接调用。
1.1 为什么要使用面向抽象编程?
尽量引用高层类型,避免引用实际子类型的方式。
上句的解释是,使用向上转型引用具体的子类实例,可以忽略具体的子类型。也可以理解为,面向抽象编程使得调用者只关心抽象方法的定义,不关心子类的具体实现。
|
|
比如上面例子中的抽象方法 sound() 定义为动物的叫声,Dog 类 和 Cat 类都是抽象类 AbstractAnimal 的子类,通过向上转型的对象 dog 和 cat 可以直接调用方法 sound(),并不关心 AbstractAnimal 类型变量的具体子类型(即不关心动物是如何叫的,只关心动物的叫声是什么)。
这样的好处是,假如引用一个新的子类 Fox,在上面代码中添加:
|
|
调用时不用关心新的子类是如何实现 sound() 方法,也不用修改抽象类中的任何代码。这也是实现多态的其中一种方法。
2. 接口
如果说抽象类中可以定义抽象方法,那么接口就是抽象方法的集合。
关于接口,有以下的规定:
-
接口并不是类,类描述对象的属性和方法,接口则包含类要实现的方法。
-
接口中 只能包含常量的声明和抽象方法,不存在构造方法,所有常量默认都是
public static final,所有方法默认都是public abstract,以上均可省略。 -
所有非抽象类实现一个接口时,须使用
implements关键字,而且接口中的所有方法必须覆写实现,访问权限也必须是public。 -
接口支持多继承,即一个类可以实现多个接口。
2.1 抽象类与接口的比较
| 语法维度 | 抽象类 | 接口 |
|---|---|---|
| 关系 | is - a | can - do |
| 定义关键字 | abstract | interface |
| 子类继承或实现关键字 | extends | implements |
| 方法实现 | 可以有 | 不能有(JDK8 开始支持 default) |
| 方法访问控制符 | 无限制 | 有限制,默认是 public abstract 类型 |
| 属性访问控制符 | 无限制 | 有限制,默认是 public static final 类型 |
| 静态方法 | 可以有 | 不能有(JDK8 开始可以有) |
| static{} 静态代码块 | 可以有 | 不能有 |
| 本类型之间拓展 | 单继承 | 多继承 |
| 本类型之间拓展关键字 | extends | extends |
对于上述表格的一些解释:
-
抽象类中如果只有一个抽象方法,那么它等同于一个接口。
-
抽象类能继承抽象类,接口能继承接口,但是只有接口可以同时继承多个接口,表示该接口可以做很多事情,使用的关键字为
extends而非implements。 -
在 JDK8 开始支持
default方法,当接口中新增一个default方法,可以不必影响所有的实现该接口的类,可以按需覆写。default方法可以在接口中实现,可以在不被覆写的情况下直接调用。
如下定义了默认方法 fly(),如果定义了 Bird 类就可以在调用或覆写该方法,其它实现该接口的类不必覆写。众所周知猫和狗不能飞,若使用抽象方法定义了 fly(),则在 Dog 和 Cat 类都需要覆写,这时覆写的方法变得无意义。
|
|
-
如果一个类实现了多个接口,而这些接口又有同名的 default 方法,该类必须覆写接口中的 default 方法,否则无法指明是哪个接口的 default 方法。
-
如果子类 B 继承父类 A,父类 A 中有 a 方法,该子类同时实现的接口中也有被 default 修饰的 a 方法,那么子类 B 会继承父类 A 的 a 方法而不是继承接口中的 a 方法。
2.2 接口回调
对于上述例子中,主类是使用类的对象调用接口覆写的方法。如果使用接口变量的引用指向实现接口的类的对象,那么该接口变量就可以调用被类实现的接口方法。
将主类改为:
|
|
结果一致:
|
|
接口变量 animal 调用被各类实现的方法时,相当于通知相应的对象调用该方法。接口回调的作用类似向上转型对象调用子类覆写的方法。
补充:非访问修饰符 final 和 static
上文甚至之前文中的代码使用了 final 和 static 修饰符,static 变量和方法已经在前文记录,下作补充。
1. final 的三个“不能”:
-
final修饰的类不能被继承。 -
final修饰的方法不能被子类覆写。 -
final修饰的字段和局部变量不能被重新赋值。
2. static 变量
-
局部变量不能被声明为
static变量。 -
实例对象没有静态字段。最好通过
类名.变量来访问类变量。 -
一个类的所有实例下的静态字段 共享同一片空间,无论修改哪个实例下的静态字段,都会影响到所有实例的静态字段,例如:
|
|
结果为:
|
|
可见对 person2 的编号赋值 12 后会将 person1 的编号从 17 改成 12。实际上,上面的主函数相当于:
|
|
日常使用 static 变量时需要特别注意,因为不同于 final,静态字段是可以重新赋值的。