先简单过一下接口语法中的注意点。
注意点
-
接口中的变量隐式指定为
public static final
-
接口中的方法会被隐式指定为
public abstract
(JDK 1.9 后允许 private,其它修饰符会报错)这也决定了接口中所有方法都必须被实现,当然这一要求有两种特殊的满足方式——抽象类实现接口,那么接口方法不一定要实现,可以由抽象类的子类实现;JDK 1.8 后有默认实现的接口方法也不必被实现类显式实现。
-
接口不能有构造方法
-
JDK 1.8 后,接口可以有静态方法和方法体
-
JDK 1.8 后,接口方法可以有默认方法,用
default
关键字修饰
与抽象类语法上的区别
-
一个类可以实现多个接口
-
一个接口可以继承多个其它接口
Java 接口是对行为的抽象,一个行为本身可以看作多个行为的耦合
public interface Hockey extends Sports, Event
接口与抽象类区别
接口和抽象类语法上的不同在之前两个文档中都已经接释清楚了,这里主要看两者思想上的不同。
摘取一些《Effective Java》中的说法
接口是对行为的抽象,达到 API 定义与实现分类 的目的,因此支持多实现。甚至可以用没有任何方法的接口,作为 Marker Interface,目的仅仅是进行声明。但是用接口导出常量是不合适的使用,接口应当尽量减少细节泄露,常量应当由类保管。
相较之下,抽象类的主要目的是 代码重用,本质是不能实例化的类。
重点有两个——抽象类和接口的本质、目的都不同。
本质
抽象类本质是类,因此不能多继承。C++ 允许多继承而 Java 不允许,这体现两者多态思想上的差别。我们知道 Java 继承其实叫 extends
,严格来讲不叫“继承”,因为在 Java 中,继承首先是一种 is-a
关系,即 Student
要继承 Person
,首先要满足 “Student
is a
Person
” 。这很好理解,按照里氏替换原则 (Liskov Substitution),进行继承关系抽象时,凡是可以用父类或者基类的地方,都可以用子类进行替换。因此这显然不是中文里面继承的语义,因为中文里“儿子继承父亲”,但儿子不可能是父亲,两者有本质区别。
然后,Java 为什么只允许单继承就很好理解了。如果我想让 A 同时继承 B 和 C,那说明 A is B, A is C,那 B 和 C 之间自然应当满足某种继承关系,三者应当是一个继承链的关系而非继承树的关系,通过单继承也可以表示清楚,并不需要让 A 同时继承 B 和 C。更不用说 C++ 为了实现多继承,其实也带来菱形继承问题,可能造成内存浪费和数据冗余。
C++中因为允许多继承,可能会因为菱形继承造成内存浪费和数据冗余(如两个类BC分别继承同一基类A,再从这两个类派生出一个类D时会有冗余成员),因此最好使用虚继承。虚继承下,D实例内存地址中,BC虚继承来的A部分会通过一个指针分别指向一张虚基表(准确来讲是指向其中的虚基表偏移指针的存储地址,然后通过该指针取出偏移量),从虚基表中取出从基类A虚继承来成员在D内存中的偏移地址。
曾看到有博客认为 C++ 多继承机制较为合理,给出的理由是“人可以有父母,那类也应当可以多继承”。结合前文内容,这个理由显然没有什么道理,但是我认为却很好地反映了 Java 中接口的思想。我们知道,对于生物而言,父和母显然是不等价的,母完成了作为母的职责,父完成了作为父的职责,然后才有“子”,因此与其类比为继承,不如类比为接口实现更妥当,因为父、母实际上在这里归根结底是行为的抽象,父类必须实现接口 CanBeFather
,母类必须实现接口 CanBeMother
,这是一种行为关系。这一接口在使用过程中屏蔽了其它底层细节,无论父母是什么学历、有什么资产,这一过程中都不关注,也没有影响,这便是类功能上的解耦,通过一个用来将行为抽象化的接口完成了。
目的
目的上面其实也说了,《Effective Java》总结的很到位,接口是对行为的抽象,达到 API 定义与实现分类 的目的。如果一个类可以有多个行为、实现多个功能,那它当然可以实现多个接口。
而抽象类更多还是用来减少冗余代码,换句话说——提高代码质量、提高可读性、降低维护难度……
Java 继承与里氏替换原则
Java中子类重写父类方法时不能抛出父类中没有抛出的异常,编译会不过,不抛是完全可以的。同时,该方法在子类中的访问级别也不能低于超类中的访问级别。这两项规则确保可使用超类实例的地方也能使用子类实例,符合里氏替换原则。
- 变量只能被隐藏(静态及非静态),不能被覆盖
- 静态方法只能被隐藏,不能覆盖
- 静态方法只能用静态方法隐藏,非静态方法只能用非静态方法覆盖(否则编译不过)
- 最终方法不能覆盖。私有方法(private)实际会被隐式指定为 final ,所以同样不能覆盖。
- 抽象方法必须覆盖
同时要注意的是关于构造方法的内容
- 子类实例化对象时,如果子类构造方法没有显式调用父类构造方法,默认调用
super()
- 子类要使用父类有参构造方法,使用
super(...)
形式,且super()
必须是子类构造方法中第一行语句 - 父类没有不带参构造方法,子类构造方法中必须显示调用父类其它构造方法,否则编译不过