Java 接口中的默认方法,用户希望在接口中提供方法的实现,将接口方法声明为 default
,并以常规方式添加实现。Java 之所以不支持多继承(multiple inheritance),是为了避免所谓的钻石问题(diamond problem)。
Java 接口中的默认方法 问题描述
用户希望在接口中提供方法的实现。
Java 接口中的默认方法 解决方案
将接口方法声明为 default
,并以常规方式添加实现。
Java 接口中的默认方法 具体实例
Java 之所以不支持多继承(multiple inheritance),是为了避免所谓的钻石问题(diamond problem)。考虑如图 1-2 所示的继承层次结构(有点类似 UML)。
图 1-2:Animal
继承
Animal
类包括 Bird
和 Horse
两个子类,二者重写了 Animal
的 speak
方法:Horse
是“嘶嘶”(whinny),而 Bird
是“唧唧”(chirp)。那么 Pegasus
(从 Horse
和 Bird
继承而来)5 呢?如果将 Animal
类型的引用赋给 Pegasus
的实例会怎样? speak
方法又该返回什么呢?
5“一匹长有双翼的骏马。”(源自迪士尼电影《大力士海格力斯》,你不会没听说过希腊神话和海格力斯吧?)
Animal animal = new Pegaus();
animal.speak(); // 嘶嘶、唧唧还是其他声音?
不同语言处理这个问题的方法各不相同。例如,C++ 支持多继承,但如果某个类继承了相互冲突的实现则不会被编译。6 而在 Eiffel7 中,编译器允许用户选择所需的实现。
6但仍然可以使用虚继承(virtual inheritance)来解决这个问题。
7Eiffel 或许对读者来说略显晦涩,它是面向对象编程的基础语言之一。感兴趣的话,可以参考 Bertrand Meyer 撰写的 Object-Oriented Software Construction, Second Edition,该书由 Prentice Hall 于 1997 年出版。
Java 禁止多继承。为避免一个类与多种类型都具有“某种”关系,Java 引入接口作为解决方案。由于接口只包含抽象方法,不会存在相互冲突的实现。接口之所以允许多继承,是因为只有方法签名被继承。
问题在于,如果永远无法在接口中实现方法,就会导致一些奇怪的情况出现。以 java.util.Collection
接口为例,它定义了以下方法:
boolean isEmpty()
int size()
如果集合中没有元素,isEmpty
方法将返回 true
,否则返回 false
。而 size
方法返回集合中元素的数量。如例 1-25 所示,无论底层实现如何,都可以根据 size
立即实现 isEmpty
方法。
例 1-25 根据
size
实现isEmpty
方法
public boolean isEmpty() {
return size() == 0;
}
由于 Collection
是一个接口,不能对它进行这样的处理,但可以使用 Java 标准库提供的 java.util.AbstractCollection
类。它是一个抽象类,所包含的 isEmpty
方法与本例中 isEmpty
的实现完全相同。如果用户正在创建自定义的集合实现(collection implementation)且还没有超类,可以通过继承 AbstractCollection
类来获得 isEmpty
方法。不过如果已有超类,就必须改为实现 Collection
接口,且不要忘记提供自定义的 isEmpty
和 size
实现。
这些对经验丰富的 Java 开发人员而言很容易,但从 Java 8 开始,情况有所改变。目前只须将某个方法声明为 default
并提供一个实现,就能为接口方法添加实现。如例 1-26 所示,Employee
接口包含两种抽象方法和一种默认方法。
例 1-26
Employee
接口包含默认方法
public interface Employee {
String getFirst();
String getLast();
void convertCaffeineToCodeForMoney();
default String getName() { ➊
return String.format("%s %s", getFirst(), getLast());
}
}
➊ 具有实现的默认方法
getName
方法由关键字 default
声明,其实现取决于 Employee
接口的另外两种抽象方法,即 getFirst
和 getLast
。
为保持向后兼容性,Java 的许多现有接口都采用默认方法进行了增强。一般而言,为接口添加新方法会破坏所有现有的实现。如果添加的新方法被声明为默认方法,则所有现有的实现将继承新方法且仍然有效。这使得库维护者可以在 JDK 中添加新的默认方法,而不会破坏现有的实现。
例如,java.util.Collection
接口目前包含以下默认方法:
default boolean removeIf(Predicate<? super E> filter)
default Stream<E> stream()
default Stream<E> parallelStream()
default Spliterator<E> spliterator()
removeIf
方法将删除集合中所有满足 Predicate
8 参数的元素,如果删除了任何元素,该方法将返回 true
。stream
和 parallelStream
方法用于创建流,二者属于工厂方法。spliterator
方法从实现 Spliterator
接口的类中返回一个对象,它对来自源的元素进行遍历和分区。
8Predicate
是 java.util.function
包新增的一种函数式接口,相关讨论请参见范例 Predicate接口 。
如例 1-27 所示,默认方法与其他方法的用法并无二致。
例 1-27 默认方法的应用
List<Integer> nums = new ArrayList<>();
nums.add(-3);
nums.add(1);
nums.add(4);
nums.add(-1);
nums.add(5);
nums.add(9);
boolean removed = nums.removeIf(n -> n <= 0); ➊
System.out.println("Elements were " + (removed ? "" : "NOT") + " removed");
nums.forEach(System.out::println); ➋
❶ 使用 Collection
接口定义的默认方法 removeIf
❷ 使用 Iterator
接口定义的默认方法 forEach
如果一个类采用同一种默认方法实现了两个接口,会出现什么情况呢?范例 默认方法冲突 将讨论这个问题,不过简而言之,类可以实现方法本身。详细信息请参见范例 默认方法冲突 。