4.6  默认方法

Collection接口中增加了新的stream方法,如何能让MyCustomList类在不知道该方法的情况下通过编译?Java 8通过如下方法解决该问题:Collection接口告诉他所有的子类:“如果你没有实现stream方法,就使用我的吧。”接口中这样的方法叫作默认方法,在任何接口中,无论函数接口还是非函数接口,都可以使用该方法。

Iterable接口中也新增了一个默认方法:forEach,该方法功能和for循环类似,但是允许用户使用一个Lambda表达式作为循环体。例4-10展示了JDK中forEach的实现方式:

例4-10 默认方法示例:forEach实现方式

  1. default void forEach(Consumer<? super T> action) {
  2. for (T t : this) {
  3. action.accept(t);
  4. }
  5. }

如果已经习惯了通过调用接口方法来使用Lambda表达式的方式,那么这个例子理解起来就相当简单。它使用一个常规的for循环遍历Iterable对象,然后对每个值调用accept方法。

既然如此简单,为何还要单独提出来呢?重点就在于代码段前面的新关键字default。这个关键字告诉javac用户真正需要的是为接口添加一个新方法。除了添加了一个新的关键字,默认方法在继承规则上和普通方法也略有区别。

和类不同,接口没有成员变量,因此默认方法只能通过调用子类的方法来修改子类本身,避免了对子类的实现做出各种假设。

默认方法和子类

默认方法的重写规则也有一些微妙之处。从最简单的情况开始来看:没有重写。在例4-11中,Parent接口定义了一个默认方法welcome,调用该方法时,发送一条信息。ParentImpl类没有实现welcome方法,因此它自然继承了该默认方法。

例4-11 Parent接口,其中的welcome是一个默认方法

  1. public interface Parent {
  2. public void message(String body);
  3. public default void welcome() {
  4. message("Parent: Hi!");
  5. }
  6. public String getLastMessage();
  7. }

在例4-12中调用代码,我们调用默认方法,可以看到断言正确。

例4-12 在客户代码中使用默认方法

  1. @Test
  2. public void parentDefaultUsed() {
  3. Parent parent = new ParentImpl();
  4. parent.welcome();
  5. assertEquals("Parent: Hi!", parent.getLastMessage());
  6. }

这时可新建一个接口Child,继承自Parent接口,代码如例4-13所示。Child接口实现了自己的默认welcome方法,凭直觉判断可知,该方法重写了Parent的方法。同样在这个例子中,ChildImpl类不会实现welcome方法,因此它自然也继承了接口的默认方法。

例4-13 继承了Parent接口的Child接口

  1. public interface Child extends Parent {
  2. @Override
  3. public default void welcome() {
  4. message("Child: Hi!");
  5. }
  6. }

此时的类继承体系如图4-4所示。

4.6  默认方法 - 图1

图4-4:类继承体系图

例4-14调用了该接口,最后输出的字符串自然是"Child: Hi!"

例4-14 调用Child接口的客户代码

  1. @Test
  2. public void childOverrideDefault() {
  3. Child child = new ChildImpl();
  4. child.welcome();
  5. assertEquals("Child: Hi!", child.getLastMessage());
  6. }

现在默认方法成了虚方法——和静态方法刚好相反。任何时候,一旦与类中定义的方法产生冲突,都要优先选择类中定义的方法。例4-15和例4-16展示了这种情况,最终调用的是OverridingParent的,而不是Parentwelcome方法。

例4-15 重写welcome默认实现的父类

  1. public class OverridingParent extends ParentImpl {
  2. @Override
  3. public void welcome() {
  4. message("Class Parent: Hi!");
  5. }
  6. }

例4-16 调用的是类中的具体方法,而不是默认方法

  1. @Test
  2. public void concreteBeatsDefault() {
  3. Parent parent = new OverridingParent();
  4. parent.welcome();
  5. assertEquals("Class Parent: Hi!", parent.getLastMessage());
  6. }

例4-18展示了另一种情况,或许不认为类中重写的方法能够覆盖默认方法。OverridingChild本身并没有任何操作,只是继承了ChildOverridingParent中的welcome方法。最后,调用的是OverridingParent中的welcome方法,而不是Child接口中定义的默认方法(代码如例4-17所示),原因在于,与接口中定义的默认方法相比,类中重写的方法更具体(参见图4-5)。

例4-17 子接口重写了父接口中的默认方法

  1. public class OverridingChild extends OverridingParent implements Child {
  2. }

例4-18 类中重写的方法优先级高于接口中定义的默认方法

  1. @Test
  2. public void concreteBeatsCloserDefault() {
  3. Child child = new OverridingChild();
  4. child.welcome();
  5. assertEquals("Class Parent: Hi!", child.getLastMessage());
  6. }

4.6  默认方法 - 图2

图4-5:完整的继承体系图

简言之,类中重写的方法胜出。这样的设计主要是由增加默认方法的目的决定的,增加默认方法主要是为了在接口上向后兼容。让类中重写方法的优先级高于默认方法能简化很多继承问题。

假设已实现了一个定制的列表MyCustomList,该类中有一个addAll方法,如果新的List接口也增加了一个默认方法addAll,该方法将对列表的操作代理到add方法。如果类中重写的方法没有默认方法的优先级高,那么就会破坏已有的实现。