Java 接口中的默认方法

Java 接口中的默认方法,用户希望在接口中提供方法的实现,将接口方法声明为 default,并以常规方式添加实现。Java 之所以不支持多继承(multiple inheritance),是为了避免所谓的钻石问题(diamond problem)。

Java 接口中的默认方法 问题描述

用户希望在接口中提供方法的实现。

Java 接口中的默认方法 解决方案

将接口方法声明为 default,并以常规方式添加实现。

Java 接口中的默认方法 具体实例

Java 之所以不支持多继承(multiple inheritance),是为了避免所谓的钻石问题(diamond problem)。考虑如图 1-2 所示的继承层次结构(有点类似 UML)。

图 1-2:Animal 继承
Animal 类包括 BirdHorse 两个子类,二者重写了 Animalspeak 方法:Horse 是“嘶嘶”(whinny),而 Bird 是“唧唧”(chirp)。那么 Pegasus(从 HorseBird 继承而来)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 接口,且不要忘记提供自定义的 isEmptysize 实现。
这些对经验丰富的 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 接口的另外两种抽象方法,即 getFirstgetLast
为保持向后兼容性,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 方法将删除集合中所有满足 Predicate8 参数的元素,如果删除了任何元素,该方法将返回 truestreamparallelStream 方法用于创建流,二者属于工厂方法。spliterator 方法从实现 Spliterator 接口的类中返回一个对象,它对来自源的元素进行遍历和分区。
8Predicatejava.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
如果一个类采用同一种默认方法实现了两个接口,会出现什么情况呢?范例 默认方法冲突 将讨论这个问题,不过简而言之,类可以实现方法本身。详细信息请参见范例 默认方法冲突

赞(4)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

Java 实例