Java|Java 8 | 默认方法

在前面的文章中,我们学习了 Lambda 表达式和函数式接口。接下来我们继续讨论与之相关的特性 --- 默认方法。
首先我们先来回顾一下 Java 8 之前,关于接口有哪些特性:

  • 接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。
  • 接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)。
  • 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。
在了解 Java 8 的默认方法后,我们对于接口的认识可能会有比较大的变化。
什么是默认方法
Default methods enable you to add new functionality to the interfaces of your libraries and ensure binary compatibility with code written for older versions of those interfaces.
正如名字所表达的,Java 8 中的默认方法就是指默认的方法(这不是废话吗)。如果我们不去重载这些方法,这些方法仍然能够被调用。注意,默认方法是定义在接口中的,不能用在抽象类中。
举个例子:
public interface Moveable { default void move(){ System.out.println("I am moving"); } }

Moveable 接口定义了 move() 方法,并且提供了默认实现。任何实现了 Moveable 接口的类可以不必实现自己的 move() 方法就能通过 instancem.move() 方法调用。
public class Animal implements Moveable{ public static void main(String[] args){ Animal tiger = new Animal(); tiger.move(); } } Output: I am moving

当然,任何类也可以自己实现 move() 方法。
ublic class Animal implements Moveable{public void move(){ System.out.println("I am running"); }public static void main(String[] args){ Animal tiger = new Animal(); tiger.move(); } } Output: I am running

那么默认方法有什么好处呢?
  1. 静态默认方法:除了默认方法,Java 8 还允许在接口中定义静态方法。所有实现了这个接口的实例都能访问到这个静态默认方法。这给库中的类提供工具方法带来了便利。
  2. 给接口扩展功能带来了方便。如果需要给接口添加新的功能,默认方法的加入可以不必让实现了该接口的类必须实现新加的接口方法。这一点在下面会提到。
Java 中为什么需要默认方法?
在 java 8 之前,接口与其实现类之间的耦合度太高了,当需要为一个接口添加方法时,所有的实现类都必须随之修改。默认方法的引入打破了这个尴尬的局面。它可以为接口添加新的方法,而不会破坏已有的接口的实现。这在 lambda 表达式作为 java 8 语言的重要特性而出现之际,为升级旧接口且保持向后兼容提供了途径。
我们拿 JDK 中的类来说明一下。下面这个方法是在 Java 8 中新加到 java.lang.Iterable 中的。
default void forEach(Consumer action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } }

在 Java 8 之前我们迭代一个集合可能会这样:
List list = new ArrayList(); list.add(new User()); list.add(new User()); list.add(new User()); Iterator it = list.iterator(); while (it.hasNext()) { User user = it.next(); // do something }

使用 Java 8,我们对集合的迭代:
List list = new ArrayList(); list.add(new User()); list.add(new User()); list.add(new User()); list.forEach((user) -> { // do something });

这个 forEach 方法是 JDK 8 新增的接口默认方法,正是因为有了默认方法的引入,才不会因为 Iterable 接口中添加了 forEach 方法就需要修改所有 Iterable 接口的实现类。
默认方法的继承
和其它方法一样,接口默认方法也可以被继承。
interface InterfaceA { default void foo() { System.out.println("InterfaceA foo"); } }interface InterfaceB extends InterfaceA { }interface InterfaceC extends InterfaceA { @Override default void foo() { System.out.println("InterfaceC foo"); } }interface InterfaceD extends InterfaceA { @Override void foo(); }public class Test { public static void main(String[] args) { new InterfaceB() {}.foo(); // 打印:“InterfaceA foo” new InterfaceC() {}.foo(); // 打印:“InterfaceC foo” new InterfaceD() { @Override public void foo() { System.out.println("InterfaceD foo"); } }.foo(); // 打印:“InterfaceD foo”// 或者使用 lambda 表达式 ((InterfaceD) () -> System.out.println("InterfaceD foo")).foo(); } }

接口默认方法的继承分三种情况(分别对应上面的 InterfaceB 接口、InterfaceC 接口和 InterfaceD 接口):
  • 不覆写默认方法,直接从父接口中获取方法的默认实现。
  • 覆写默认方法,这跟类与类之间的覆写规则相类似。
  • 覆写默认方法并将它重新声明为抽象方法,这样新接口的子类必须再次覆写并实现这个抽象方法。
默认方法的多继承
我们知道 Java 是不支持多继承的,但是类可以实现多个接口,Java 8 中添加了默认方法后,这就带来了默认方法的多继承问题。
interface InterfaceA { default void foo() { System.out.println("InterfaceA foo"); } }interface InterfaceB { default void bar() { System.out.println("InterfaceB bar"); } }interface InterfaceC { default void foo() { System.out.println("InterfaceC foo"); }default void bar() { System.out.println("InterfaceC bar"); } }class ClassA implements InterfaceA, InterfaceB { }// 错误 //class ClassB implements InterfaceB, InterfaceC { //}class ClassB implements InterfaceB, InterfaceC { @Override public void bar() { InterfaceB.super.bar(); // 调用 InterfaceB 的 bar 方法 InterfaceC.super.bar(); // 调用 InterfaceC 的 bar 方法 System.out.println("ClassB bar"); // 做其他的事 } }

在 ClassA 类中,它实现的 InterfaceA 接口和 InterfaceB 接口中的方法不存在歧义,可以直接多实现。
在 ClassB 类中,它实现的 InterfaceB 接口和 InterfaceC 接口中都存在相同签名的 foo 方法,需要手动解决冲突。覆写存在歧义的方法,并可以使用 InterfaceName.super.methodName(); 的方式手动调用需要的接口默认方法。
需要注意一种特殊情况:
interface InterfaceA { default void foo() { System.out.println("InterfaceA foo"); } }interface InterfaceB extends InterfaceA { @Override default void foo() { System.out.println("InterfaceB foo"); } }// 正确 class ClassA implements InterfaceA, InterfaceB { }class ClassB implements InterfaceA, InterfaceB { @Override public void foo() { //InterfaceA.super.foo(); // 错误 InterfaceB.super.foo(); } }

当 ClassA 类多实现 InterfaceA 接口和 InterfaceB 接口时,不会出现方法名歧义的错误。当 ClassB 类覆写 foo 方法时,无法通过 InterfaceA.super.foo(); 调用 InterfaceA 接口的 foo 方法。
【Java|Java 8 | 默认方法】因为 InterfaceB 接口继承了 InterfaceA 接口,那么 InterfaceB 接口一定包含了所有 InterfaceA 接口中的字段方法,因此一个同时实现了 InterfaceA 接口和 InterfaceB 接口的类与一个只实现了 InterfaceB 接口的类完全等价。
这很好理解,就相当于 class SimpleDateFormat extends DateFormat 与 class SimpleDateFormat extends DateFormat, Object 等价(如果允许多继承)。
或者换种方式理解:
class ClassC { public void foo() { System.out.println("ClassC foo"); } }class ClassD extends ClassC { @Override public void foo() { System.out.println("ClassD foo"); } }public class Test { public static void main(String[] args) { ClassC classC = new ClassD(); classC.foo(); // 打印:“ClassD foo” } }

这里的 classC.foo(); 同样调用的是 ClassD 类中的 foo 方法,打印结果为“ClassD foo”,因为 ClassC 类中的 foo 方法在 ClassD 类中被覆写了。
在前面的 ClassA 类中不会出现方法名歧义的原因是所谓“存在歧义”的方法其实都来自于 InterfaceA 接口,InterfaceB 接口中的“同名方法”只是继承自 InterfaceA 接口而来并对其进行了覆写。ClassA 类实现的两个接口不是两个毫不相干的接口,因此不存在同名歧义方法。
而覆写意味着对父类方法的屏蔽,这也是 Override 的设计意图之一。因此在实现了 InterfaceB 接口的类中无法访问已被覆写的 InterfaceA 接口中的 foo 方法。
这是当接口继承行为发生冲突时的规则之一,即 被其它类型所覆盖的方法会被忽略。
如果想要调用 InterfaceA 接口中的 foo 方法,只能通过自定义一个新的接口同样继承 InterfaceA 接口并显示地覆写 foo 方法,在方法中使用 InterfaceA.super.foo(); 调用 InterfaceA 接口的 foo 方法,最后让实现类同时实现 InterfaceB 接口和自定义的新接口,代码如下:
interface InterfaceA { default void foo() { System.out.println("InterfaceA foo"); } }interface InterfaceB extends InterfaceA { @Override default void foo() { System.out.println("InterfaceB foo"); } }interface InterfaceC extends InterfaceA { @Override default void foo() { InterfaceA.super.foo(); } }class ClassA implements InterfaceB, InterfaceC { @Override public void foo() { InterfaceB.super.foo(); InterfaceC.super.foo(); } }

注意! 虽然 InterfaceC 接口的 foo 方法只是调用了一下父接口的默认实现方法,但是这个覆写 不能省略,否则 InterfaceC 接口中继承自 InterfaceA 接口的隐式的 foo 方法同样会被认为是被 InterfaceB 接口覆写了而被屏蔽,会导致调用 InterfaceC.super.foo() 时出错。
通过这个例子,应该注意到在使用一个默认方法前,一定要考虑它是否真的需要。因为 默认方法会带给程序歧义,并且在复杂的继承体系中容易产生编译错误。滥用默认方法可能给代码带来意想不到、莫名其妙的错误。
接口与抽象类
当接口继承行为发生冲突时的另一个规则是,类的方法声明优先于接口默认方法,无论该方法是具体的还是抽象的。
interface InterfaceA { default void foo() { System.out.println("InterfaceA foo"); }default void bar() { System.out.println("InterfaceA bar"); } }abstract class AbstractClassA { public abstract void foo(); public void bar() { System.out.println("AbstractClassA bar"); } }class ClassA extends AbstractClassA implements InterfaceA { @Override public void foo() { InterfaceA.super.foo(); } }public class Test { public static void main(String[] args) { ClassA classA = new ClassA(); classA.foo(); // 打印:“InterfaceA foo” classA.bar(); // 打印:“AbstractClassA bar” } }

ClassA 类中并不需要手动覆写 bar 方法,因为优先考虑到 ClassA 类继承了的 AbstractClassA 抽象类中存在对 bar 方法的实现,同样的因为 AbstractClassA 抽象类中的 foo 方法是抽象的,所以在 ClassA 类中必须实现 foo 方法。
虽然 Java 8 的接口的默认方法就像抽象类,能提供方法的实现,但是他们俩仍然是 不可相互代替的:
  • 接口可以被类多实现(被其他接口多继承),抽象类只能被单继承。
  • 接口中没有 this 指针,没有构造函数,不能拥有实例字段(实例变量)或实例方法,无法保存 状态(state),抽象方法中可以。
  • 抽象类不能在 java 8 的 lambda 表达式中使用。
  • 从设计理念上,接口反映的是 “like-a” 关系,抽象类反映的是 “is-a” 关系。
其他注意点
  • default 关键字只能在接口中使用(以及用在 switch 语句的 default 分支),不能用在抽象类中。
  • 接口默认方法不能覆写 Object类的 equals、hashCode 和 toString 方法。
  • 接口中的静态方法必须是 public 的,public 修饰符可以省略,static 修饰符不能省略。

    推荐阅读