default默认方法(之前也被称为虚拟扩展方法或守护方法)
lambda表达式和方法引用大大提升了Java的表达能力(expressiveness),不过为了使把代码即数据(code-as-data)变的更加容易,我们需要把这些特性融入到已有的库之中,以便开发者使用。
Java SE 7时代为一个已有的类库增加功能是非常困难的。具体的说,接口在发布之后就已经被定型,除非我们能够一次性更新所有该接口的实现,否则向接口添加方法就会破坏现有的接口实现。默认方法(之前被称为虚拟扩展方法或守护方法)的目标即是解决这个问题,使得接口在发布之后仍能被逐步演化。
这里给出一个例子,我们需要在标准集合API中增加针对lambda的方法。例如removeAll方法应该被泛化为接收一个函数式接口Predicate,但这个新的方法应该被放在哪里呢?我们无法直接在Collection接口上新增方法——不然就会破坏现有的Collection实现。我们倒是可以在Collections工具类中增加对应的静态方法,但这样就会把这个方法置于“二等公民”的境地。
默认方法利用面向对象的方式向接口增加新的行为。它是一种新的方法:接口方法可以是抽象的或是默认的。默认方法拥有其默认实现,实现接口的类型通过继承得到该默认实现(如果类型没有覆盖该默认实现)。此外,默认方法不是抽象方法,所以我们可以放心的向函数式接口里增加默认方法,而不用担心函数式接口的单抽象方法限制。
下面的例子展示了如何向Iterator接口增加默认方法skip:
interface Iterator<E> {
boolean hasNext();
E next();
void remove();
default void skip(int i) {
for ( ; i > 0 && hasNext(); i -= 1) next();
}
}
根据上面的Iterator定义,所有实现Iterator的类型都会自动继承skip方法。在使用者的眼里,skip不过是接口新增的一个虚拟方法。在没有覆盖skip方法的Iterator子类实例上调用skip会执行skip的默认实现:调用hasNext和next若干次。子类可以通过覆盖skip来提供更好的实现——比如直接移动游标(cursor),或是提供为操作提供原子性(Atomicity)等。
当接口继承其它接口时,我们既可以为它所继承而来的抽象方法提供一个默认实现,也可以为它继承而来的默认方法提供一个新的实现,还可以把它继承而来的默认方法重新抽象化。
除了默认方法,Java SE 8还在允许在接口中定义静态方法。这使得我们可以从接口直接调用和它相关的辅助方法(Helper method),而不是从其它的类中调用(之前这样的类往往以对应接口的复数命名,例如Collections)。比如,我们一般需要使用静态辅助方法生成实现Comparator的比较器,在Java SE 8中我们可以直接把该静态方法定义在Comparator接口中:
public static <T, U extends Comparable<? super U>>
Comparator<T> comparing(Function<T, U> keyExtractor) {
return (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
和其它方法一样,默认方法也可以被继承,大多数情况下这种继承行为和我们所期待的一致。不过,当类型或者接口的超类拥有多个具有相同签名的方法时,我们就需要一套规则来解决这个冲突:
1.类的方法(class method)声明优先于接口默认方法。无论该方法是具体的还是抽象的。
2.被其它类型所覆盖的方法会被忽略。这条规则适用于超类型共享一个公共祖先的情况。
为了演示第二条规则,我们假设Collection和List接口均提供了removeAll的默认实现,然后Queue继承并覆盖了Collection中的默认方法。在下面的implement从句中,List中的方法声明会优先于Queue中的方法声明:
class LinkedList<E> implements List<E>, Queue<E> { ... }
当两个独立的默认方法相冲突或是默认方法和抽象方法相冲突时会产生编译错误。这时程序员需要显式覆盖超类方法。一般来说我们会定义一个默认方法,然后在其中显式选择超类方法:
interface Robot implements Artist, Gun {
default void draw() { Artist.super.draw(); }
}
super前面的类型必须是有定义或继承默认方法的类型。这种方法调用并不只限于消除命名冲突——我们也可以在其它场景中使用它。
最后,接口在inherits和extends从句中的声明顺序和它们被实现的顺序无关。