1.7 默认方法
1.7 默认方法
许多开发语言都将函数表达式集成到了其集合库中。这样比循环方式所需的代码更少,并且更加容易理解。以下面的循环为例:
- for (int i = 0; i < list.size(); i++)
- System.out.println(list.get(i));
事实上有一种更好的方式。API 开发人员可以提供一个forEach 方法,用来将一个函数应用到集合的每个元素上。下面是使用这种方式编写的一个简单调用:
- list.forEach(System.out::println);
如果集合库是完全重新设计的,这样做不会有什么问题。但是,Java 的集合库是许多年以前设计的,这就会带来一个问题。如果Collection 接口添加了新的方法,例如forEach,那么每个实现了Collection 接口的自定义类就必须都实现该方法。这在Java中是完全无法接受的。
Java 设计者们希望通过允许接口包含带有具体实现的方法(称为默认方法)来一劳永逸地解决这个问题。这些方法可以被安全地添加到已有的接口中。在本节中,我们将详细讲解默认方法。
注意:在Java 8 中,通过将要在本节中介绍的机制,forEach 方法已经被添加到Iterable 接口中(它是Collection 接口的一个父接口)。
假设有如下接口:
- interface Person {
- long getId();
- default String getName() { return "John Q. Public"; }
- }
该接口有两个方法:一个抽象方法getId,以及一个默认方法getName。当然,实现Person 接口的具体类必须实现getId 方法,但是它可以选择保留getName 的实现,或者重写它。
默认方法终结了以前的一种经典模式,即提供一个接口,以及一个实现接口的大多数或全部方法的抽象类, 例如Collection/AbstractCollection 或WindowListener/WindowAdapter。现在你只需要在接口中实现那些方法。
如果一个接口中定义了一个默认方法,而另外一个父类或接口中又定义了一个同名的方法,该选择哪个呢?像Scala 和C++等语言可能会有一套复杂的规则来解决这种二义性,但是幸运的是,Java 中的规则要简单得多,如下所示:
1. 选择父类中的方法。如果一个父类提供了具体的实现方法,那么接口中具有相同名称和参数的默认方法会被忽略。
2. 接口冲突。如果一个父接口提供了一个默认方法,而另一个接口也提供了一个具有相同名称和参数类型的方法(不管该方法是否是默认方法),那么你必须通过覆盖该方法来解决冲突。
我们来详细理解一下第二条规则。假定另一个接口也含有一个名为getName 的方法:
- interface Named {
- default String getName() { return getClass().getName() + "_" + hashCode(); }
- }
如果你编写了一个同时实现这两个接口的类,会发生什么事呢?
- class Student implements Person, Named {
- ...
- }
该类会继承由Person 和Named 接口同时提供的getName 方法,但是这两个方法的实现并不一致。Java 编译器会报告一个错误,并交由开发人员来解决这种冲突,而不会自动选择其中一个。对于这种情况,你只需要在Student 类中提供一个getName 方法,在该方法中再选择调用其中一个接口中的方法,如下所示:
- class Student implements Person, Named {
- public String getName() { return Person.super.getName(); }
- ...
- }
现在我们假定Named 接口没有提供getName 方法的一个默认实现:
- interface Named {
- String getName();
- }
如果这样,Student 类能继承Person 接口中的默认方法吗?也许这说得过去,但是Java 设计者们为了保持统一,还是选择了与之前一样的处理方式。两个接口如何冲突并不重要,只要有一个接口提供了实现,编译器就会报告一个错误,而开发人员必须手动解决这种冲突。
注意:当然,如果两个接口都没有为共享方法提供一个默认的实现,那么我们就又回到了Java 8 之前的情况,也就不存在什么冲突。这时实现类有两种选择,即实现该方法或者不实现该方法。如果选择后者,该类本身就是一个抽象类。
我们刚刚讨论了两个接口之间的冲突。现在我们考虑这样一个类,它继承了某个父类并实现了某个接口,而这个父类和接口中都有一个同名的方法。例如,假设Person是一个类,而Student 类的定义如下所示:
- class Student extends Person implements Named { ... }
在这种情况下,只有父类中的方法会起作用,接口中的任何默认方法都会被忽略。在这个例子中,不管Named 接口中的getName 方法是否是默认方法,Student 都会继承Person 类中的getName 方法。这就是“类优先”的规则。
“类优先”规则可以保证与Java 7 的兼容性。如果你在接口中添加了一个默认方法,它对Java 8 以前编写的代码不会产生任何影响。
小心:你不能为Object 中的方法重新定义一个默认方法。例如,你不能定义一个默认方法toString 或者equals,即使这对于List 这样的接口很有吸引力。“ 类优先” 规则的结果是, 这样的方法永远不可能优先于Object.toString 或者Object.equals。