Java8实战-总结39
- 默认方法
- 解决冲突的规则
- 解决问题的三条规则
- 选择提供了最具体实现的默认方法的接口
- 冲突及如何显式地消除歧义
- 菱形继承问题
- 小结
默认方法
解决冲突的规则
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();
}
}
菱形继承问题中一个类同时继承了具有相同函数签名的两个方法。到底该选择哪一个实现呢? Java 8
提供了解决这个问题的方案。
解决问题的三条规则
如果一个类使用相同的函数签名从多个地方(比如另一个类或接口)继承了方法,通过三条规则可以进行判断。
- 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
- 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果
B
继承了A
,那么B
就比A
更加具体。 - 如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。
选择提供了最具体实现的默认方法的接口
回顾一下开头的例子,这个例子中C
类同时实现了B
接口和A
接口,而这两个接口恰巧又都定义了名为hello
的默认方法。另外,B
继承自A
。下图是这个场景的UML
图。
按照规则2,编译器会使用选择的是提供了最具体实现的默认方法的接口。由于B
比A
更具体,所以应该选择B
的hello
方法。所以,程序会打印输出“Hello from B
”。
如果C
像下面这样(如下图)继承自D
,会发生什么情况:
public class D implements A{ }
public class C extends D implements B, A {
public static void main(String... args) {
new C().hello();
}
}
依据规则1,类中声明的方法具有更高的优先级。D
并未覆盖hello
方法,可是它实现了接口A
。所以它就拥有了接口A
的默认方法。规则2说如果类或者父类没有对应的方法,那么就应该选择提供了最具体实现的接口中的方法。因此,编译器会在接口A
和接口B
的hello
方法之间做选择。由于B
更加具体,所以程序会再次打印输出“Hello from B
”。
牢记这些判断的规则
在这个测验中继续复用之前的例子,唯一的不同在于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方法。
冲突及如何显式地消除歧义
到目前为止,你看到的这些例子都能够应用前两条判断规则解决。让我们更进一步,假设B
不再继承A,(如下图所示):
public interface A {
void hello() {
System.out.println("Hello from A");
}
}
public interface B {
void hello() {
System.out.println("Hello from B");
}
}
public class C implements B, A { }
这时规则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();
}
}
几乎完全一样的函数签名这个测试中,假设接口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
无法通过编译的原因。
菱形继承问题
最后一种场景,亦是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();
}
}
下图以UML
图的方式描述了出现这种问题的场景。这种问题叫“菱形问题”,因为类的继承关系图形状像菱形。这种情况下类D
中的默认方法到底继承自什么地方 ——源自B
的默认方法,还是源自C
的默认方法?实际上只有一个方法声明可以选择。只有A
声明了一个默认方法。由于这个接口是D
的父接口,代码会打印输出“Hello from A
”。
现在,看看另一种情况,如果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对象的副本中反映出来。
如果一个类的默认方法使用相同的函数签名继承自多个接口,解决冲突的机制其实相当简单。只需要遵守下面这三条准则就能解决所有可能的冲突。
- 首先,类或父类中显式声明的方法,其优先级高于所有的默认方法。
- 如果用第一条无法判断,方法签名又没有区别,那么选择提供最具体实现的默认方法的接口。
- 最后,如果冲突依旧无法解决,就只能在你的类中覆盖该默认方法,显式地指定在类中使用哪一个接口中的方法。
小结
Java 8
中的接口可以通过默认方法和静态方法提供方法的代码实现。- 默认方法的开头以关键字
default
修饰,方法体与常规的类方法相同。 - 向发布的接口添加抽象方法不是源码兼容的。
- 默认方法的出现能帮助库的设计者以后向兼容的方式演进
API
。 - 默认方法可以用于创建可选方法和行为的多继承。
- 有办法解决由于一个类从多个接口中继承了拥有相同函数签名的方法而导致的冲突。
- 类或者父类中声明的方法的优先级高于任何默认方法。如果前一条无法解决冲突,那就选择同函数签名的方法中实现得最具体的那个接口的方法。
- 两个默认方法都同样具体时,你需要在类中覆盖该方法,显式地选择使用哪个接口中提供的默认方法。