当继承、接口出现代码冲突
一、代码冲突
看到代码冲突,也许你的第一反应是某个项目多人协作,操作同一分支而导致代码冲突。“醉翁之意不在酒”,本文的代码冲突是指当一个类继承某个类,实现多个接口,某个类与接口都具有相同的函数签名,这种情况下,类会选择使用哪一个函数?在实际情况中,像这样的冲突可能极少发生,但是一旦发生这样的状况,必须要有一套规则来确定按照什么样的约定处理这些冲突。
备注信息:JDK:1.8
二、解决问题的三条规则
如果一个类使用相同的函数签名从多个地方(比如另一个类或接口)继承了方法,通过三条
规则可以进行判断。
类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体。
最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。
1. 选择提供了最具体实现的默认方法的接口
我们来看看刚才举的例子,这个例子中C类同时实现了B接口和A接口,而这两个接口恰巧又都定义了名为hello的默认方法。另外, B继承自A。UML图如下:
按照规则2,应该选择的是提供了最具体实现的默认方法的接口。由于B比A更具体,所以应该选择B的hello方法。所以,程序会打印输出“B”。
既然B比A更具体,如果我们在A的基础上再具体一点,例如D继承A
1 | public class D implements A { |
1 | public class C extends D implements B, A { |
UML图如下:
依据规则1,类中声明的方法具有更高的优先级。 D并未覆盖hello方法,可是它实现了接口A。所以它就拥有了接口A的默认方法。规则2说如果类或者父类没有对应的方法,那么就应该选择提供了最具体实现的接口中的方法。因此,编译器会在接口A和接口B的hello方法之间做选择。由于B更加具体,所以程序会再次打印输出“B”。
如果我们在D中重写了A接口中的hello方法,则会执行D中的函数,因为符合规则1。
1 | public class D implements A { |
2. 冲突及如何显式地消除歧义
一个类 C 实现 A、B 两个接口,A、B 具有相同的函数签名。
1 | public interface A { |
1 | public interface B { |
1 | public class C implements B, A { |
我们看类 C ,你觉得会执行哪个方法?为什么呢?实际上类 C 中的代码是无法编译的。 这时规则2就无法进行判断了,因为从编译器的角度看没有哪一个接口的实现更加具体,两个都差不多。 A接口和B接口的hello方法都是有效的选项。所以, Java编译器这时就会抛出一个编译错误,因为它无法判断哪一个方法更合适:“Error: class C inherits unrelated defaults for hello() from types B and A.”
冲突解决
解决这种两个可能的有效方法之间的冲突,没有太多方案;你只能显式地决定你希望在C中使用哪一个方法。为了达到这个目的,你可以覆盖类C中的hello方法,在它的方法体内显式地调用你希望调用的方法。 Java 8中引入了一种新的语法X.super.m(…),其中X是你希望调用的m方法所在的父接口。举例来说,如果你希望C使用来自于B的默认方法,它的调用方式看起来就如下所示:
1 | public class C implements A, B { |
3. 菱形继承问题
1 | public interface A{ |
UML图如下:
这种问题叫“菱形问题”,因为类的继承关系图形状像菱形。这种情况下类D中的默认方法到底继承自什么地方 ——源自B的默认方法,还是源自C的默认方法?实际上只有一个方法声明可以选择。只有A声明了一个默认方法。由于这个接口是D的父接口,代码会打印输出“Hello from A”。
现在,我们看看另一种情况,如果B中也提供了一个默认的hello方法,并且函数签名跟A中的方法也完全一致,这时会发生什么情况呢?根据规则2,编译器会选择提供了更具体实现的接口中的方法。由于B比A更加具体,所以编译器会选择B中声明的默认方法。如果B和C都使用相同的函数签名声明了hello方法,就会出现冲突,正如我们之前所介绍的,你需要显式地指定使用哪个方法。
说明:本文参考《Java8 实战》第九章,默认方法;