前言
本章简单介绍接口的使用,以及 Java 8 新增的一些特性。
版本约定
- JDK Version:11.0.12
- Java SE API Documentation:https://docs.oracle.com/en/java/javase/11/docs/api/index.html
正文
接口的特性
- 接口不是类,不能使用 new 运算符实例化一个接口。
接口中的所有方法自动地属于 public。因此,在接口中声明方法时,不必提供关键字 public。
public interface Comparable<T> {
public int compareTo(T o);
}
接口中声明的域将被自动设为 public static final,即是常量。
public interface SwingConstants {
public static final int CENTER = 0;
......
}
静态方法
在 Java 8 中,允许在接口中增加静态方法。那么这么做的意义是什么呢?
以前的做法都是将静态方法放在伴随类中。在标准库中,你会看到成对出现的接口和实用工具类,如 Collection/Collections 或 Path/Paths。
在 Java 8 出来以后,因为可以在接口中增加静态方法,我们在实现自己的接口时,就不再需要为实用工具方法另外提供一个伴随类了。
比如 List 接口提供了如下静态方法:
public interface List<E> extends Collection<E> {
static <E> List<E> copyOf(Collection<? extends E> coll) {
return ImmutableCollections.listCopy(coll);
}
......
}
默认方法
可以为接口方法提供一个默认实现。必须用 default 修饰符标记这样一个方法。
public interface List<E> extends Collection<E> {
default void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final ListIterator<E> li = this.listIterator();
while (li.hasNext()) {
li.set(operator.apply(li.next()));
}
}
......
}
默认方法怎么定义我们知道了,接下来讲讲默认方法适用于哪些场景。
第一种场景:例如,如果希望在发生鼠标点击事件时得到通知,就要实现一个包含 5 个方法的接口:
public interface MouseListener {
void mouseClicked(MouseEvent event);
void mousePressed(MouseEvent event);
void mouseReleased(MouseEvent event);
void mouseEntered(MouseEvent event);
void mouseExited(MouseEvent event);
}
但是,大多数情况下,我们只需要关心其中的 1、2 个事件类型。如果是在 Java 8 以前,我们不得不实现所有的方法。当 Java 8 出来后,我们可以把所有方法声明为默认方法,这些默认方法什么也不做。
public interface MouseListener {
default void mouseClicked(MouseEvent event) {};
default void mousePressed(MouseEvent event) {};
default void mouseReleased(MouseEvent event) {};
default void mouseEntered(MouseEvent event) {};
default void mouseExited(MouseEvent event) {};
}
这样,在实现这个接口的时候,只需要覆盖真正关心的事件。
第二种场景:默认方法可以调用任何其他方法。例如,Collection 接口可以定义一个便利方法:
public interface Collection {
int size();
default boolean isEmpty() {
return size() == 0;
}
}
这样 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 解决默认方法冲突的规则如下:
- 超类优先。如果超类提供了一个具体的方法,同名而且有相同参数类型的默认方法会被忽略。
- 接口冲突。如果两个接口提供了一个同名而且参数类型相同的方法,则实现了这两个接口的类必须覆盖这个方法来解决冲突。
先看第一个规则。一个类扩展了一个超类,同时实现了一个接口,并从超类和接口继承了相同的方法。例如,有一个超类 Person 和一个接口 Named,都包含 getName 方法。
public class Person {
public String getName() {
return getClass().getName() + "_" + hashCode();
}
}
public interface Named {
default String getName() {
return getClass().getName() + "_" + hashCode();
}
}
Student 类继承了 Person 类,同时实现了 Named 接口。
public class Student extends Person implements Named {
......
}
在这种情况下,只会考虑超类方法,接口的所有默认方法都会被忽略。在我们的例子中,Student 从 Person 继承了 getName 方法,Named 接口是否为 getName 提供了默认实现并不会带来什么区别。这正是“类优先”规则。
所以,千万不要让一个默认方法重新定义 Object 类中的某个方法。例如,不能为 toString 或 equals 定义默认方法,尽管对于 List 之类的接口这可能很有吸引力。由于“类优先”规则,这样的定义是无效的,它绝对无法超越 Object.toString 或 Objects.equals。
另外,“类优先”规则可以确保与 Java 7 的兼容性。如果为一个接口增加默认方法,这对于有这个默认方法之前能正常工作的代码不会有任何影响。
下面来看第二个规则。例如,有两个接口 Person 和 Named,都包含 getName 默认方法。
public interface Person {
default String getName() {
return getClass().getName() + "_" + hashCode();
}
}
public interface Named {
default String getName() {
return getClass().getName() + "_" + hashCode();
}
}
如果有一个 Student 类同时实现了这两个接口会怎么样呢?
public class Student implements Person, Named {
......
}
Student 类会继承 Person 和 Named 接口提供的两个不一致的 getName 方法。这时 Java 编译器会提示一个错误,需要我们来解决这个二义性。解决方法也很简单,只需要在 Student 类中提供一个 getName 方法。
public class Student implements Person, Named {
@Override
public String getName() {
return Person.super.getName();
}
}
现在假设 Named 接口没有为 getName 提供默认实现:
public interface Named {
String getName();
}
Student 类会从 Person 接口继承默认方法吗?
不会的,Java 设计者更强调一致性。两个接口如何冲突并不重要。只要有一个接口提供了实现,编译器就会提示错误,而我们就必须解决这个二义性。
当然,如果两个接口都没有为共享方法提供默认实现,那么就与 Java 8 之前的情况一样,这里不存在冲突。 实现类可以有两个选择:实现这个方法,或者干脆不实现。如果是后一种情况,这个类本身就是抽象的。
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/vrev7i 来源:殷建卫 - 开发笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。