Java 8 接口中的默认方法和静态方法
本章节源代码:
https://github.com/java-8/java8-impatient/tree/master/src/main/java/study/java8/trait
The purpose of default methods […] is to enable interfaces to be evolved in a compatible manner after their initial publication.
(Brian Goetz – Sep 2013)
Java 8从两个概念扩展了接口的定义:
默认方法和静态方法
默认方法使得接口有点类似于Traits语法但是面向的目标不同。它允许添加新方法到已有接口中,但是不会破坏那些基于老版接口实现的代码的二进制兼容性。
默认方法和抽象方法的区别在于:抽象方法是必须要实现的,而默认方法不是。
相反,每个接口必须提供一个所谓的默认方法实现,所有的实现类都会默认继承得到这个方法(如果需要也可以重写这个默认实现)。我们来看看下面这个例子:
/**
* InterfaceWithDefaultMethod.java
*/
package study.java8.trait;
/**
*
* @author jack 2016年8月10日 下午3:51:19
*/
public interface InterfaceWithDefaultMethod {
abstract public void abstractMethod();
public static void staticMethod() {
System.out.println("staticMethod print");
}
public default void defaultMethod() {
System.out.println("defaultMethod print");
}
public static void main(String[] args) {// 接口可以有main函数
staticMethod();
// InterfaceWithDefaultMethod iMethod = new
// InterfaceWithDefaultMethod(); 接口不能new实例化
// defaultMethod();
}
}
/**
* InterfaceWithDefaultMethodImpl.java
*/
package study.java8.trait;
/**
* @author jack 2016年8月10日 下午3:57:06
*/
public class InterfaceWithDefaultMethodImpl implements InterfaceWithDefaultMethod {
public static void main(String[] args) {
InterfaceWithDefaultMethodImpl impl = new InterfaceWithDefaultMethodImpl();
impl.defaultMethod();
impl.abstractMethod();
// impl.staticMethod(); // 调不到接口里面的静态方法, The method staticMethod() is
// undefined for the type InterfaceWithDefaultMethodImpl
InterfaceWithDefaultMethod.staticMethod();// 类似scala里面的object的意思
}
/*
* (non-Javadoc)
*
* @see study.java8.trait.InterfaceWithDefaultMethod#abstractMethod()
*/
@Override
public void abstractMethod() {
// Do Something ...
System.out.println("Do Something ...");
}
}
设计的初衷
这样看起来,接口和类的界限就有点不明显了,同时也会带来多继承,菱形问题。这样设计的初衷是什么?
由于java8开始支持lambda表达式,可以把函数当做参数传递,最明显的lambda表达式应用场景莫过于对collection的每一个元素应用lambda。如果想为Collection实现lambda表达式:
list.forEach(…); // 这就是lambda代码
首先想到的是为Collection的父接口iterator添加抽象方法forEach()。然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。如果添加了,那么所有的iterator()实现类都要重写这个方法,如果只是jre自己的类库还好说,大量的第三方类库都使用到了java的优秀集合框架,如果都要重写,这是不合理的。
因此,如果在Java 8里使用lambda的时候,因为向前兼容的原因而不能用于collection库,那有多糟糕啊。
由于上述原因,引入了一个新的概念。虚拟扩展方法,也即通常说的defender方法, 现在可以将其加入到接口,这样可以提供声明的行为的默认实现。
简单的说,Java的接口现在可以实现方法了。默认方法带来的好处是可以为接口添加新的默认方法,而不会破坏接口的实现。
之前:Java接口纯粹是契约的集合,是一种程序设计的表达方式。从数据抽象的角度看,能够在不定义class的同时又可以定义type,将是程序设计中强大而有用的机制。Java接口就是这些纯粹的接口组成的数据抽象。Java接口只能够拥有抽象方法,它不涉及任何实现,也不能创建其对象(这一点和抽象类一致)。
多重继承模型导致额外的复杂性,其中最著名的是钻石问题或者叫“讨嫌的菱形派生”(Dreadful Diamond onDerivation、DDD)。为什么Java接口能够避免多继承的复杂性,关键在于它仅仅包含abstract方法。然而从设计的角度看,Java接口放弃了多继承的内在/固有目标,而显得是一个权宜之计。
现在:Java8之前,接口不能升级。因为在接口中添加一个方法,会导致老版本接口的所有实现类的中断。λ表达式作为核心出现,为了配合λ表达式,JDK中Collection库需要添加新的方法,如forEach(),stream()等,于是引入了默认方法(defender methods,Virtual extension methods)。它是库/框架设计的程序员的后悔药。对于以前的遗留代码,大家都不知道有这个新方法,既不会调用,也不会去实现,如同不存在;编写新代码的程序员可以将它视为保底的方法体。类型层次中任何符合override规则的方法,优先于默认方法,因为遗留代码可能正好有同样的方法存在。
默认方法,理论上抹杀了Java接口与抽象类的本质区别——前者是契约的集合,后者是接口与实现的结合体。当然,语法上两者的差别和以前一样。这就需要程序员来自觉维护两者的本质区别,把默认方法作为库、框架向前兼容的手段。
默认方法的一个好处:多继承的著名的是钻石问题(The Diamond Problem )再次需要关注。因而使以前某些人认为的“为了解决多继承问题而引入接口机制”的说法变成明显的错误——以前也是错误的认识。
默认方法的声明很简单,直接在接口中把方法声明为default,之后再写方法的实现即可。这样所有的实现类都会继承这个方法,问题是他带来的多继承问题如何解决?
Iterable中的默认方法:
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
下面是一个简单的默认方法实现:
public interface A {
default void foo(){
System.out.println("Calling A.foo()");
}
}
public class Clazz implements A {
}
Clazz clazz = new Clazz();
clazz.foo(); // 调用A.foo()
下面是一个多继承:
public interface A {
default void foo(){
System.out.println("Calling A.foo()");
}
}
public interface B {
default void foo(){
System.out.println("Calling B.foo()");
}
}
public class Clazz implements A, B {
}
这段代码不能编译 有以下原因:
java:class Clazz 从types A到B给foo()继承了不相关的默认值
为了修复这个,在Clazz里我们不得不手动解决通过重写冲突的方法,或者调用某个接口中的方法:
public class Clazz implements A, B {
public void foo(){}
}
public class Clazz implements A, B {
public void foo(){
A.super.foo();
}
}
光剑说
JVM中默认方法的实现非常高效,而且方法调用的字节码指令支持默认方法。默认方法使得已有的Java接口能够改进而不会导致编译过程失败。接口java.util.Collection中新增了大量的方法,都是很好的示例,如: stream(), parallelStream(),forEach(), removeIf(),等等。
虽然默认方法很强大,我们还是要谨慎使用它:在将一个方法定义为default方法之前,最好三思是不是必须要这么做,因为它可能在层级复杂的情况下引起歧义和编译错误。