乐趣区

java-8-实战读书笔记-第九章-默认方法

如果在现存的接口上引入了非常多的新方法,所有的实现类都必须进行改造,实现新方法,为了解决这个问题,Java 8 为了解决这一问题引入了一种新的机制。Java 8 中的接口现在支持在声明方法的同时提供实现,这听起来让人惊讶!通过两种方式可以完成这种操作。其一,Java 8 允许在接口内声明 静态方法 。其二,Java 8 引入了一个新功能,叫 默认方法,通过默认方法你可以指定接口方法的默认实现。

静态方法可以存在于接口内部

一、不断演进的 API

默认方法试它让类库的设计者放心地改进应用程序接口,无需担忧对遗留代码的影响,这是因为实现更新接口的类现在会自动继承一个默认的方法实现。

不同类型的兼容性:二进制、源代码和函数行为
变更对 Java 程序的影响大体可以分成三种类型的兼容性,分别是:二进制级的兼容、源代码级的兼容,以及函数行为的兼容。

  • 向接口添加新方法是二进制级的兼容,但最终编译实现接口的类时却会发生编译错误。二进制级的兼容性表示现有的二进制执行文件能无缝持续链接(包括验证、准备和解析)和运行。比如,为接口添加一个方法就是二进制级的兼容,这种方式下,如果新添加的方法不被调用,接口已经实现的方法可以继续运行,不会出现错误。
  • 简单地说,源代码级的兼容性表示引入变化之后,现有的程序依然能成功编译通过。
  • 最后,函数行为的兼容性表示变更发生之后,程序接受同样的输入能得到同样的结果。比如,为接口添加新的方法就是函数行为兼容的,因为新添加的方法在程序中并未被调用(抑或该接口在实现中被覆盖了)。

二、概述默认方法

默认方法由 default 修饰符修饰,并像类中声明的其他方法一样包含方法体。比如,你可以像下面这样在集合库中定义一个名为 Sized 的接口,在其中定义一个抽象方法 size,以及一个默认方法 isEmpty:

public interface Sized {int size(); 
 default boolean isEmpty() {return size() == 0; 
 } 
}

第 3 章介绍的很多函数式接口,比如 Predicate、Function 以及 Comparator 也引入了新的默认方法,比如 Predicate.and 或者 Function.andThen(记住,函数式接口只包含一个抽象方法,默认方法是种非抽象方法)。

个抽象类可以通过实例变量(字段)保存一个通用状态,而接口是不能有实例变量的。

三、默认方法的使用模式

1. 可选方法

你很可能也碰到过这种情况,类实现了接口,不过却刻意地将一些方法的实现留白。我们以 Iterator 接口为例来说。Iterator 接口定义了 hasNext、next,还定义了 remove 方法。Java 8 之前,由于用户通常不会使用该方法,remove 方法常被忽略。因此,实现 Interator 接口的类通常会为 remove 方法放置一个空的实现,这些都是些毫无用处的模板代码。

采用默认方法之后,你可以为这种类型的方法提供一个默认的实现,这样实体类就无需在自己的实现中显式地提供一个空方法。比如,在 Java 8 中,Iterator 接口就为 remove 方法提供了一个默认实现,如下所示:

interface Iterator<T> {boolean hasNext(); 
 T next(); 
 default void remove() {throw new UnsupportedOperationException(); 
 }

2. 行为的多继承

  1. 类型的多继承
  2. 利用正交方法的精简接口
  3. 组合接口

继承不应该成为你一谈到代码复用就试图倚靠的万精油。比如,从一个拥有 100 个方法及字段的类进行继承就不是个好主意,因为这其实会引入不必要的复杂性。你完全可以使用代理有效地规避这种窘境,即创建一个方法通过该类的成员变量直接调用该类的方法。

四、解决冲突的规则

1. 解决问题的三条规则

如果一个类使用相同的函数签名从多个地方(比如另一个类或接口)继承了方法,通过三条规则可以进行判断。

  • (1)类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
  • (2)如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果 B 继承了 A,那么 B 就比 A 更加具体。
  • (3)最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。

2. 冲突及如何显式地消除歧义

对于上面提到的第三种情况,解决这种两个可能的有效方法之间的冲突,没有太多方案;你只能显式地决定你希望在 C 中使用哪一个方法。为了达到这个目的,你可以覆盖类 C 中的 hello 方法,在它的方法体内显式地调用你希望调用的方法。Java 8 中引入了一种新的语法 X.super.m(…),其中 X 是你希望调用的 m 方法所在的父接口。举例来说,如果你希望 C 使用来自于 B 的默认方法,它的调用方式看起来就如下所示:

public class C implements B, A {void hello(){B.super.hello(); 
 } 
}

3. 菱形继承问题

让我们考虑最后一种场景,它亦是 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();} 
} 

这种问题叫“菱形问题”,因为类的继承关系图形状像菱形。这种情况下类 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 显式地添加实现,否则该程序无法通过编译

退出移动版