13.4 解决冲突的规则
我们知道Java语言中一个类只能继承一个父类,但是一个类可以实现多个接口。随着默认方法在Java 8中引入,有可能出现一个类继承了多个方法但它们使用的是同样的函数签名。这种情况下,类会选择使用哪一个函数?在实际情况中,像这样的冲突可能极少发生,但是一旦发生这样的状况,必须要有一套规则来确定按照什么样的约定处理这些冲突。本节会介绍Java编译器如何解决这种潜在的冲突。我们试图回答像“接下来的代码中,哪一个hello方法是被C类调用的”这样的问题。注意,接下来的例子主要用于说明容易出问题的场景,并不表示这些场景在实际开发过程中会经常发生。
public interface A {default void hello() {System.out.println("Hello from A");}}public interface B extends A {default void hello() {System.out.println("Hello from B");}}public class C implements B, A {public static void main(String... args) {new C().hello(); ←---- 猜猜打印输出的是什么?}}
此外,你可能早就对C++语言中著名的菱形继承问题有所了解,菱形继承问题中一个类同时继承了具有相同函数签名的两个方法。到底该选择哪一个实现呢?Java 8也提供了解决这个问题的方案。请接着阅读下面的内容。
13.4.1 解决问题的三条规则
如果一个类使用相同的函数签名从多个地方(比如另一个类或接口)继承了方法,那么通过三条规则可以进行判断。
(1) 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
(2) 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体。
(3) 最后,如果还是无法判断,那么继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。
我们保证,这些就是你需要知道的全部!下面来看几个例子。
13.4.2 选择提供了最具体实现的默认方法的接口
回顾一下本节开头的例子,这个例子中C类同时实现了B接口和A接口,而这两个接口恰巧又都定义了名为hello的默认方法。另外,B继承自A。图13-5是这个场景的UML图。

图 13-5 提供最具体的默认方法实现的接口,其优先级更高
编译器会使用声明的哪一个hello方法呢?按照规则(2),应该选择的是提供了最具体实现的默认方法的接口。由于B比A更具体,因此应该选择B的hello方法。所以,程序会打印输出“Hello from B”。
现在,来看一下如果C像下面这样(如图13-6所示)继承自D,会发生什么情况。
public class D implements A{ }public class C extends D implements B, A {public static void main(String... args) {new C().hello(); ←---- 猜猜打印输出的是什么?}}

图 13-6 继承一个类,实现两个接口的情况
依据规则(1),类中声明的方法具有更高的优先级。D并未覆盖hello方法,可是它实现了接口A。所以它就拥有了接口A的默认方法。规则(2)说如果类或者父类没有对应的方法,那么就应该选择提供了最具体实现的接口中的方法。因此,编译器会在接口A和接口B的hello方法之间做选择。因为B更加具体,所以程序会再次打印输出“Hello from B”。你可以继续尝试测验13.2,考察一下对这些规则的理解。
测验13.2:牢记这些判断的规则
我们在这个测验中继续复用之前的例子,唯一的不同在于
D现在显式地覆盖了从A接口中继承的hello方法。你认为现在的输出会是什么呢?
public class D implements A{void hello(){System.out.println("Hello from D");}}public class C extends D implements B, A {public static void main(String… args) {new C().hello();}}答案:由于依据规则(1),父类中声明的方法具有更高的优先级,因此程序会打印输出“Hello from D”。
注意,
D的声明如下:
public abstract class D implements A {public abstract void hello();}这样的结果是,虽然在结构上其他的地方已经声明了默认方法的实现,但是
C还是必须提供自己的hello方法。
13.4.3 冲突及如何显式地消除歧义
到目前为止,你看到的这些例子都能够应用前两条判断规则解决。让我们更进一步,假设B不再继承A(如图13-7所示)。
public interface A {default void hello() {System.out.println("Hello from A");}}public interface B {default void hello() {System.out.println("Hello from B");}}public class C implements B, A { }

图 13-7 同时实现具有相同函数声明的两个接口
这时规则(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的默认方法,它的调用方式看起来就如下所示:
public class C implements B, A {void hello(){B.super.hello(); ←---- 显式地选择调用接口B中的方法}}
继续看看测验13.3,这是一个相关但更加复杂的例子。
测验13.3:几乎完全一样的函数签名
这个测验中,假设接口
A和B的声明如下所示:
public interface A{default Number getNumber(){return 10;}}public interface B{default Integer getNumber(){return 42;}}类
C的声明如下:
public class C implements B, A {public static void main(String… args) {System.out.println(new C().getNumber());}}这个程序会打印输出什么呢?
答案:类
C无法判断A或者B到底哪一个更加具体。这就是类C无法通过编译的原因。
13.4.4 菱形继承问题
让我们考虑最后一种场景,它亦是C++中最令人头痛的难题。
public interface A{default void hello(){System.out.println("Hello from A");}}public interface B extends A { }public interface C extends A { }public class D implements B, C {public static void main(String... args) {new D().hello(); ←---- 猜猜打印输出的是什么?}}
图13-8以UML图的方式描述了出现这种问题的场景。这种问题叫菱形问题,因为类的继承关系图形状似菱形。这种情况下类D中的默认方法到底继承自什么地方 ——源自B的默认方法,还是源自C的默认方法?实际上只有一个方法声明可以选择。只有A声明了一个默认方法。由于这个接口是D的父接口,因此代码会打印输出“Hello from A”。

图 13-8 菱形问题
现在,来看看另一种情况,如果B中也提供了一个默认的hello方法,并且函数签名跟A中的方法也完全一致,那这时会发生什么情况呢?根据规则(2),编译器会选择提供了更具体实现的接口中的方法。由于B比A更加具体,因此编译器会选择B中声明的默认方法。如果B和C都使用相同的函数签名声明了hello方法,就会出现冲突。正如之前所介绍的,你需要显式地指定使用哪个方法。
顺便提一句,如果你在C接口中添加一个抽象的hello方法(这次添加的不是一个默认方法),那么会发生什么情况呢?你可能也想知道答案。
public interface C extends A {void hello();}
这个新添加到C接口中的抽象方法hello比由接口A继承而来的hello方法拥有更高的优先级,因为C接口更加具体。因此,类D现在需要为hello显式地添加实现,否则该程序无法通过编译。
C++语言中的菱形问题
C++语言中的菱形问题要复杂得多。首先,C++允许类的多继承。默认情况下,如果类
D继承了类B和类C,而类B和类C又都继承自类A,那么类D实际直接访问的是B对象和C对象的副本。最后的结果是,要使用A中的方法必须显式地声明:这些方法是来自于B接口,还是C接口。此外,类也有状态,所以修改B的成员变量不会在C对象的副本中反映出来。
现在你应该已经了解了,如果一个类的默认方法使用相同的函数签名继承自多个接口,那么解决冲突的机制其实相当简单。你只需要遵守下面这三条准则就能解决所有可能的冲突。
(1) 首先,类或父类中显式声明的方法,其优先级高于所有的默认方法。
(2) 如果用第一条无法判断,方法签名又没有区别,那么选择提供最具体实现的默认方法的接口。
(3) 最后,如果冲突依旧无法解决,你就只能在你的类中覆盖该默认方法,显式地指定在你的类中使用哪一个接口中的方法。
