2.默认方法
default关键字,默认方法的引入就是为了以兼容的方式解决像Java API这样的类库的演进问题的,它让类可以自动地继承接口的一个默认实现,为接口的演进提供了一种平滑的方式,你的改动将不会导致已有代码的修改。
Java 8中的抽象类和抽象接口
那么抽象类和抽象接口之间的区别是什么呢?它们不都能包含抽象方法和包含方法体的实现吗?
- 首先,一个类只能继承一个抽象类,但是一个类可以实现多个接口。
- 其次,一个抽象类可以通过实例变量(字段)保存一个通用状态,而接口是不能有实例变量的。
默认方法的使用模式
使用默认方法的两种用例: 可选方法和行为的多继承.
可选方法:
/**
*减少无效的模板代码。实现Iterator接口的每一个类都不需要再声明一个空的remove方法了,因为它现在已经有一个默认的实现。
*/
interface Iterator<T> {
boolean hasNext();
T next();
default void remove() {
throw new UnsupportedOperationException();
}
}
行为的多继承
默认方法让之前无法想象的事儿以一种优雅的方式得以实现,即行为的多继承。这是一种让类从多个来源重用代码的能力.由于Java 8中接口方法可以包含实现,类可以从多个接口中继承它们的行为(即实现的代码)。
解决冲突的规则
Java语言中一个类只能继承一个父类,但是一个类可以实现多个接口。随着默认方法在Java 8中引入,有可能出现一个类继承了多个方法而它们使用的却是同样的函数签名。这种情况下,类会选择使用哪一个函数?
如果一个类使用相同的函数签名从多个地方(比如另一个类或接口)继承了方法,通过三条规则可以进行判断。
(1) 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
(2) 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体。
(3) 最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。
选择提供了最具体实现的默认方法的接口
C类同时实现了B接口和A接口,而这两个接口恰巧又都定义了名为hello的默认方法。另外, B继承自A。编译器会使用声明的哪一个hello方法呢?按照规则(2),应该选择的是提供了最具体实现的默认方法的接口。由于B比A更具体,所以应该选择B的hello方法。
依据规则(1),类中声明的方法具有更高的优先级。 D并未覆盖hello方法,可是它实现了接口A。所以它就拥有了接口A的默认方法。规则(2)说如果类或者父类没有对应的方法,那么就应该选择提供了最具体实现的接口中的方法。因此,编译器会在接口A和接口B的hello方法之间做选择。由于B更加具体,所以程序会再次打印输出“ Hello from B”。
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) {
//由于依据规则(1),父类中声明的方法具有更高的优先级,所以程序会打印输出“ Hello from D”。
new C().hello();
}
}
/**
*虽然在结构上,其他的地方已经声明了默认方法的实现, C还是必须提供自己的hello方法。
*/
public abstract class D implements A {
public abstract void hello();
}
冲突及如何显式地消除歧义
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();//显式地选择调用接口B中的方法
}
}
菱形继承问题
现在你应该已经了解了,如果一个类的默认方法使用相同的函数签名继承自多个接口,解决
冲突的机制其实相当简单。你只需要遵守下面这三条准则就能解决所有可能的冲突。
首先,类或父类中显式声明的方法,其优先级高于所有的默认方法。
如果用第一条无法判断,方法签名又没有区别,那么选择提供最具体实现的默认方法的接口。
最后,如果冲突依旧无法解决,你就只能在你的类中覆盖该默认方法,显式地指定在你的类中使用哪一个接口中的方法。
小结:
Java 8中的接口可以通过默认方法和静态方法提供方法的代码实现。
默认方法的开头以关键字default修饰,方法体与常规的类方法相同。
向发布的接口添加抽象方法不是源码兼容的。
默认方法的出现能帮助库的设计者以后向兼容的方式演进API。
默认方法可以用于创建可选方法和行为的多继承。
我们有办法解决由于一个类从多个接口中继承了拥有相同函数签名的方法而导致的冲突。
类或者父类中声明的方法的优先级高于任何默认方法。如果前一条无法解决冲突,那选择同函数签名的方法中实现得最具体的那个接口的方法。
两个默认方法都同样具体时,你需要在类中覆盖该方法,显式地选择使用哪个接口中提供的默认方法。