13.3 默认方法的使用模式

现在你已经了解了默认方法怎样以兼容的方式演进库函数了。除了这种用例,还有其他场景也能利用这个新特性吗?当然有,你可以创建自己的接口,并为其提供默认方法。本节会介绍使用默认方法的两种用例:可选方法和行为的多继承。

13.3.1 可选方法

你很可能也碰到过这种情况,类实现了接口,却刻意地将一些方法的实现留白。我们以Iterator接口为例。Iterator接口定义了hasNextnext,还定义了remove方法。Java 8之前,由于用户通常不会使用该方法,remove方法常被忽略。因此,实现Iterator接口的类通常会为remove方法放置一个空的实现,这些都是毫无用处的模板代码。

采用默认方法之后,你可以为这种类型的方法提供一个默认的实现,这样实体类就无须在自己的实现中显式地提供一个空方法。比如,在Java 8中,Iterator接口就为remove方法提供了一个默认实现,如下所示:

  1. interface Iterator<T> {
  2. boolean hasNext();
  3. T next();
  4. default void remove() {
  5. throw new UnsupportedOperationException();
  6. }
  7. }

通过这种方式,你可以减少无效的模板代码。实现Iterator接口的每一个类都不需要再声明一个空的remove方法了,因为它现在已经有一个默认的实现。

13.3.2 行为的多继承

默认方法让之前无法想象的事以一种优雅的方式得以实现,即行为的多继承。这是一种让类从多个来源重用代码的能力,如图13-3所示。

13.3 默认方法的使用模式 - 图1

图 13-3 单继承和多继承的比较

Java的类只能继承单一的类,但是一个类可以实现多接口。要确认也很简单,下面是Java API中对ArrayList类的定义:

  1. public class ArrayList<E> extends AbstractList<E> ←---- 继承唯一一个类
  2. implements List<E>, RandomAccess, Cloneable, ←---- 但是实现了四个接口
  3. Serializable {
  4. }
  • 类型的多继承

这个例子中ArrayList继承了一个类,实现了四个接口。因此ArrayList实际是七个类型的直接子类,分别是:AbstractListListRandomAccessCloneableSerializableIterableCollection。所以,在某种程度上,我们早就有了类型的多继承。

由于Java 8中接口方法可以包含实现,因此类可以从多个接口中继承它们的行为(即实现的代码)。让我们从一个例子入手,看看如何充分利用这种能力来为我们服务。保持接口的精致性和正交性能帮助你在现有的代码基上最大程度地实现代码复用和行为组合。

  • 利用正交方法的精简接口

假设你需要为正在创建的游戏定义多个具有不同特质的形状。有的形状需要调整大小,但是不需要有旋转的功能;有的需要能旋转和移动,但是不需要调整大小。这种情况下,该怎样设计才能尽可能地重用代码?

你可以定义一个单独的Rotatable接口,并提供两个抽象方法setRotationAnglegetRotationAngle。该接口还定义了一个默认方法rotateBy,你可以通过setRotationAnglegetRotationAngle实现该方法,如下所示:

  1. public interface Rotatable {
  2. void setRotationAngle(int angleInDegrees);
  3. int getRotationAngle();
  4. default void rotateBy(int angleInDegrees){ ←---- retateBy方法的一个默认实现
  5. setRotationAngle((getRotationAngle () + angleInDegrees) % 360);
  6. }
  7. }

这种方式和模板设计模式有些相似,都是以其他方法需要实现的方法定义好框架算法。

现在,实现了Rotatable的所有类都需要提供setRotationAnglegetRotationAngle的实现,但与此同时它们也会天然地继承rotateBy的默认实现。

类似地,你可以定义之前看到的两个接口MoveableResizable。它们都包含了默认实现。下面是Moveable的代码:

  1. public interface Moveable {
  2. int getX();
  3. int getY();
  4. void setX(int x);
  5. void setY(int y);
  6. default void moveHorizontally(int distance){
  7. setX(getX() + distance);
  8. }
  9. default void moveVertically(int distance){
  10. setY(getY() + distance);
  11. }
  12. }

下面是Resizable的代码:

  1. public interface Resizable {
  2. int getWidth();
  3. int getHeight();
  4. void setWidth(int width);
  5. void setHeight(int height);
  6. void setAbsoluteSize(int width, int height);
  7. default void setRelativeSize(int wFactor, int hFactor){
  8. setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor);
  9. }
  10. }
  • 组合接口

通过组合这些接口,你现在可以为你的游戏创建不同的实体类。比如,Monster可以移动、旋转和缩放。

  1. public class Monster implements Rotatable, Moveable, Resizable {
  2. ... ←---- 需要给出所有抽象方法的实现,但无须重复实现默认方法
  3. }

Monster类会自动继承RotatableMoveableResizable接口的默认方法。这个例子中,Monster继承了rotateBymoveHorizontallymoveVerticallysetRelativeSize的实现。

你现在可以直接调用不同的方法:

  1. Monster m = new Monster(); ←---- 构造函数会设置Monster的坐标、高度、宽度及默认仰角
  2. m.rotateBy(180); ←---- 调用由Rotatable中继承而来的rotateBy
  3. m.moveVertically(10); ←---- 调用由Moveable中继承而来的moveVertically

假设你现在需要声明另一个类,它要能移动和旋转,但是不能缩放,比如说Sun。这时也无须复制粘贴代码,你可以像下面这样复用MoveableRotatable接口的默认实现。图13-4是这一场景的UML图表。

  1. public class Sun implements Moveable, Rotatable {
  2. ... ←---- 需要给出所有抽象方法的实现,但无须重复实现默认方法
  3. }

13.3 默认方法的使用模式 - 图2

图 13-4 多种行为的组合

像你的游戏代码那样使用默认实现来定义简单的接口还有另一个好处。假设你需要修改moveVertically的实现,让它更高效地运行。你可以在Moveable接口内直接修改它的实现,所有实现该接口的类会自动继承新的代码(这里假设用户并未定义自己的方法实现)。

关于继承的一些错误观点

继承不应该成为你一谈到代码复用就试图倚靠的“万精油”。比如,从一个拥有100个方法及字段的类进行继承就不是个好主意,因为这其实会引入不必要的复杂性。你完全可以使用代理有效地规避这种窘境,即创建一个方法通过该类的成员变量直接调用该类的方法。这就是为什么有的时候我们发现有些类被刻意地声明为final类型:声明为final的类不能被其他的类继承,避免发生这样的反模式,防止核心代码的功能被污染。注意,有的时候声明为final的类都会有其不同的原因,比如,String类被声明为final,因为我们不希望有人对这样的核心功能产生干扰。

这种思想同样也适用于使用默认方法的接口。通过精简的接口,你能获得最有效的组合,因为你可以只选择需要的实现。

通过前面的介绍,你已经了解了默认方法多种强大的使用模式。不过也可能还有一些疑惑:如果一个类同时实现了两个接口,这两个接口恰巧又提供了同样的默认方法签名,那这时会发生什么情况?类会选择使用哪一个方法?这些问题会在接下来的一节进行讨论。