前言

本章简单介绍接口的使用,以及 Java 8 新增的一些特性。

版本约定

  1. 接口不是类,不能使用 new 运算符实例化一个接口。
  2. 接口中的所有方法自动地属于 public。因此,在接口中声明方法时,不必提供关键字 public。

    1. public interface Comparable<T> {
    2. public int compareTo(T o);
    3. }
  3. 接口中声明的域将被自动设为 public static final,即是常量。

    1. public interface SwingConstants {
    2. public static final int CENTER = 0;
    3. ......
    4. }

    静态方法

在 Java 8 中,允许在接口中增加静态方法。那么这么做的意义是什么呢?

以前的做法都是将静态方法放在伴随类中。在标准库中,你会看到成对出现的接口和实用工具类,如 Collection/Collections 或 Path/Paths。

在 Java 8 出来以后,因为可以在接口中增加静态方法,我们在实现自己的接口时,就不再需要为实用工具方法另外提供一个伴随类了。

比如 List 接口提供了如下静态方法:

  1. public interface List<E> extends Collection<E> {
  2. static <E> List<E> copyOf(Collection<? extends E> coll) {
  3. return ImmutableCollections.listCopy(coll);
  4. }
  5. ......
  6. }

默认方法

可以为接口方法提供一个默认实现。必须用 default 修饰符标记这样一个方法。

  1. public interface List<E> extends Collection<E> {
  2. default void replaceAll(UnaryOperator<E> operator) {
  3. Objects.requireNonNull(operator);
  4. final ListIterator<E> li = this.listIterator();
  5. while (li.hasNext()) {
  6. li.set(operator.apply(li.next()));
  7. }
  8. }
  9. ......
  10. }

默认方法怎么定义我们知道了,接下来讲讲默认方法适用于哪些场景。

第一种场景:例如,如果希望在发生鼠标点击事件时得到通知,就要实现一个包含 5 个方法的接口:

  1. public interface MouseListener {
  2. void mouseClicked(MouseEvent event);
  3. void mousePressed(MouseEvent event);
  4. void mouseReleased(MouseEvent event);
  5. void mouseEntered(MouseEvent event);
  6. void mouseExited(MouseEvent event);
  7. }

但是,大多数情况下,我们只需要关心其中的 1、2 个事件类型。如果是在 Java 8 以前,我们不得不实现所有的方法。当 Java 8 出来后,我们可以把所有方法声明为默认方法,这些默认方法什么也不做。

  1. public interface MouseListener {
  2. default void mouseClicked(MouseEvent event) {};
  3. default void mousePressed(MouseEvent event) {};
  4. default void mouseReleased(MouseEvent event) {};
  5. default void mouseEntered(MouseEvent event) {};
  6. default void mouseExited(MouseEvent event) {};
  7. }

这样,在实现这个接口的时候,只需要覆盖真正关心的事件。

第二种场景:默认方法可以调用任何其他方法。例如,Collection 接口可以定义一个便利方法:

  1. public interface Collection {
  2. int size();
  3. default boolean isEmpty() {
  4. return size() == 0;
  5. }
  6. }

这样 Collection 接口的实现类就不用实现 isEmpty 方法,只需要根据自己的需求实现 size 方法。isEmpty 方法就实现了一个简单的模板方法模式。

第三种场景:在 Java API 中,很多接口都有相应的伴随类,这些伴随类实现类相应接口的部分或全部方法,比如,Collection/AbstractCollection。在 Java 8 中,可以直接在接口中实现默认方法,不用再额外新增一个伴随类。

第四种场景:默认方法的一个重要用法是“接口演化”。以 Collection 接口为例,这个接口作为 Java 的一部分已经有很多年了。假设很久以前你提供了这样一个类:public class Bag implement Collection

后来,在 Java 8 中,又为这个接口增加了一个 stream 方法。

假设 stream 方法不是一个默认方法。那么 Bag 类将不能编译,因为它没有实现这个新方法。为接口增加一个非默认方法不能保证“源代码兼容”。

不过,假设不重新编译这个类,而只是使用原先的一个包含这个类的 JAR 文件。这个类仍能正常加载,尽管没有这个新方法。程序仍然可以正常构造 Bag 实例,不会有意外发生。不过,如果程序在一个 Bag 实例上调用 stream 方法,就会出现一个 AbstractMethodError。

将方法实现为一个默认方法就可以解决这两个问题。Bag 类又能正常编译了。另外如果没有重新编译而直接加载这个类,并在一个 Bag 实例上调用 stream 方法,将调用 Collection.stream 方法。

解决默认方法冲突

如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义了同样的方法,会发生什么情况?

Java 解决默认方法冲突的规则如下:

  1. 超类优先。如果超类提供了一个具体的方法,同名而且有相同参数类型的默认方法会被忽略。
  2. 接口冲突。如果两个接口提供了一个同名而且参数类型相同的方法,则实现了这两个接口的类必须覆盖这个方法来解决冲突。

先看第一个规则。一个类扩展了一个超类,同时实现了一个接口,并从超类和接口继承了相同的方法。例如,有一个超类 Person 和一个接口 Named,都包含 getName 方法。

  1. public class Person {
  2. public String getName() {
  3. return getClass().getName() + "_" + hashCode();
  4. }
  5. }
  6. public interface Named {
  7. default String getName() {
  8. return getClass().getName() + "_" + hashCode();
  9. }
  10. }

Student 类继承了 Person 类,同时实现了 Named 接口。

  1. public class Student extends Person implements Named {
  2. ......
  3. }

在这种情况下,只会考虑超类方法,接口的所有默认方法都会被忽略。在我们的例子中,Student 从 Person 继承了 getName 方法,Named 接口是否为 getName 提供了默认实现并不会带来什么区别。这正是“类优先”规则。

所以,千万不要让一个默认方法重新定义 Object 类中的某个方法。例如,不能为 toString 或 equals 定义默认方法,尽管对于 List 之类的接口这可能很有吸引力。由于“类优先”规则,这样的定义是无效的,它绝对无法超越 Object.toString 或 Objects.equals。

另外,“类优先”规则可以确保与 Java 7 的兼容性。如果为一个接口增加默认方法,这对于有这个默认方法之前能正常工作的代码不会有任何影响。

下面来看第二个规则。例如,有两个接口 Person 和 Named,都包含 getName 默认方法。

  1. public interface Person {
  2. default String getName() {
  3. return getClass().getName() + "_" + hashCode();
  4. }
  5. }
  6. public interface Named {
  7. default String getName() {
  8. return getClass().getName() + "_" + hashCode();
  9. }
  10. }

如果有一个 Student 类同时实现了这两个接口会怎么样呢?

  1. public class Student implements Person, Named {
  2. ......
  3. }

Student 类会继承 Person 和 Named 接口提供的两个不一致的 getName 方法。这时 Java 编译器会提示一个错误,需要我们来解决这个二义性。解决方法也很简单,只需要在 Student 类中提供一个 getName 方法。

  1. public class Student implements Person, Named {
  2. @Override
  3. public String getName() {
  4. return Person.super.getName();
  5. }
  6. }

现在假设 Named 接口没有为 getName 提供默认实现:

  1. public interface Named {
  2. String getName();
  3. }

Student 类会从 Person 接口继承默认方法吗?

不会的,Java 设计者更强调一致性。两个接口如何冲突并不重要。只要有一个接口提供了实现,编译器就会提示错误,而我们就必须解决这个二义性。

当然,如果两个接口都没有为共享方法提供默认实现,那么就与 Java 8 之前的情况一样,这里不存在冲突。 实现类可以有两个选择:实现这个方法,或者干脆不实现。如果是后一种情况,这个类本身就是抽象的。

作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/vrev7i 来源:殷建卫 - 开发笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。