返回
Featured image of post Java - 接口与抽象类的区别

Java - 接口与抽象类的区别

接口和抽象类一样不能用于实例化对象,两者的差别也是一个很有嚼劲的问题.

先简单过一下接口语法中的注意点。

注意点

  • 接口中的变量隐式指定为 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() 必须是子类构造方法中第一行语句
  • 父类没有不带参构造方法,子类构造方法中必须显示调用父类其它构造方法,否则编译不过
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus