默认方法是Java 8
中引入的一个新特性,希望能借此以兼容的方式改进API
。现在,接口包含的方法签名在它的实现类中也可以不提供实现。缺失的方法实现会作为接口的一部分由实现类继承(所以命名为默认实现),而无需由实现类提供。
那么,该如何辨识哪些是默认方法呢?其实非常简单。默认方法由default
修饰符修饰,并像类中声明的其他方法一样包含方法体。比如,可以像下面这样在集合库中定义一个名为Sized
的接口,在其中定义一个抽象方法size
,以及一个默认方法isEmpty
:
public interface Sized {
int size();
//默认方法
default boolean isEmpty() {
return size() == 0;
}
}
这样任何一个实现了Sized
接口的类都会自动继承isEmpty
的实现。因此,向提供了默认实现的接口添加方法就不是源码兼容的。
现在,回顾一下最初的例子,那个Java
画图类库和你的游戏程序。具体来说,为了以兼容的方式改进这个库(即使用该库的用户不需要修改他们实现了Resizable
的类),可以使用默认方法,提供setRelativeSize
的默认实现:
default void setRelativeSize(int wFactor, int hFactor) {
setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor);
}
默认方法在Java 8
的API
中已经大量地使用了。Collection接口的stream方法就是默认方法。List接口的sort方法也是默认方法。很多函数式接口,比如Predicate、Function以及Comparator也引入了新的默认方法,比如Predicate.and或者Function.andThen(记住,函数式接口只包含一个抽象方法,默认方法是种非抽象方法)。
Java 8中的抽象类和抽象接口
那么抽象类和抽象接口之间的区别是什么呢?它们不都能包含抽象方法和包含方法体的
实现吗?
首先,一个类只能继承一个抽象类,但是一个类可以实现多个接口。
其次,一个抽象类可以通过实例变量(字段)保存一个通用状态,而接口是不能有实例变
量的。
默认方法有两种用例:可选方法和行为的多继承。
可能碰到过这种情况,类实现了接口,不过却刻意地将一些方法的实现留白。以Iterator
接口为例来说。Iterator
接口定义了hasNext、next
,还定义了remove
方法。Java 8
之前,由于用户通常不会使用该方法,remove
方法常被忽略。因此,实现Iterator
接口的类通常会为remove
方法放置一个空的实现,这些都是些毫无用处的模板代码。
采用默认方法之后,可以为这种类型的方法提供一个默认的实现,这样实体类就无需在自己的实现中显式地提供一个空方法。比如,在Java 8
中,Iterator
接口就为remove
方法提供了一个默认实现,如下所示:
interface Iterator<T> {
boolean hasNext();
T next();
default void remove() {
throw new UnsupportedOperationException();
}
}
通过这种方式,可以减少无效的模板代码。实现Iterator
接口的每一个类都不需要再声明一个空的remove
方法了,因为它现在已经有一个默认的实现。
默认方法让之前无法想象的事儿以一种优雅的方式得以实现,即行为的多继承。这是一种让类从多个来源重用代码的能力,如下图所示:
Java的类只能继承单一的类,但是一个类可以实现多接口。要确认也很简单,下面是Java API
中对ArrayList
类的定义:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable, Iterable<E>, Collection<E> {
//继承唯一一个类,但是实现了六个接口
}
ArrayList
继承了一个类,实现了六个接口。因此ArrayList
实际是七个类型的直接子类,分别是:AbstractList、List、RandomAccess、Cloneable、Serializable、Iterable
和Collection
。所以,在某种程度上,早就有了类型的多继承。由于Java 8
中接口方法可以包含实现,类可以从多个接口中继承它们的行为(即实现的代码)。从一个例子入手,看看如何充分利用这种能力来为我们服务。保持接口的精致性和正交性能帮助你在现有的代码基上最大程度地实现代码复用和行为组合。
可以定义一个单独的Rotatable
接口,并提供两个抽象方法setRotationAngle
和getRotationAngle
,如下所示:
public interface Rotatable {
void setRotationAngle(int angleInDegrees);
int getRotationAngle();
//rotateBy方法的一个默认实现
default void rotateBy(int angleInDegrees) {
setRotationAngle((getRotationAngle () + angle) % 360);
}
}
这种方式和模板设计模式有些相似,都是以其他方法需要实现的方法定义好框架算法。
现在,实现了Rotatable
的所有类都需要提供setRotationAngle
和getRotationAngle
的实现,但与此同时它们也会天然地继承rotateBy
的默认实现。
类似地,可以定义之前看到的两个接口Moveable
和Resizable
。它们都包含了默认实现。下面是Moveable
的代码:
public interface Moveable {
int getX();
int getY();
void setX(int x);
void setY(int y);
default void moveHorizontally(int distance){
setX(getX() + distance);
}
default void moveVertically(int distance){
setY(getY() + distance);
}
}
下面是Resizable
的代码:
public interface Resizable {
int getWidth();
int getHeight();
void setWidth(int width);
void setHeight(int height);
void setAbsoluteSize(int width, int height);
default void setRelativeSize(int wFactor, int hFactor) {
setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor);
}
}
Monster
可以移动、旋转和缩放。public class Monster implements Rotatable, Moveable, Resizable {
//需要给出所有抽象方法的实现,但无需重复实现默认方法
…
}
Monster
类会自动继承Rotatable、Moveable
和Resizable
接口的默认方法。这个例子中,Monster
继承了rotateBy、moveHorizontally、moveVertically
和setRelativeSize
的实现。
现在可以直接调用不同的方法:
Monster m = new Monster(); //构造函数会设置Monster的坐标、高度、宽度及默认仰角
m.rotateBy(180); //调用由 Rotatable 中继承而来的rotateBy
m.moveVertically(10); //调用由Moveable中继承而来的moveVertically
假设现在需要声明另一个类,它要能移动和旋转,但是不能缩放,比如说Sun
。这时也无需复制粘贴代码,可以像下面这样复用Moveable
和Rotatable
接口的默认实现。下图是这一场景的UML
图表。
public class Sun implements Moveable, Rotatable {
…
}
像游戏代码那样使用默认实现来定义简单的接口还有另一个好处。假设需要修改moveVertically
的实现,让它更高效地运行。可以在Moveable
接口内直接修改它的实现,所有实现该接口的类会自动继承新的代码(这里我们假设用户并未定义自己的方法实现)。
关于继承的一些错误观点
继承不应该成为你一谈到代码复用就试图倚靠的万精油。比如,从一个拥有100个方法及
字段的类进行继承就不是个好主意,因为这其实会引入不必要的复杂性。完全可以使用代理
有效地规避这种窘境,即创建一个方法通过该类的成员变量直接调用该类的方法。这就是为什
么有的时候我们发现有些类被刻意地声明为final类型:声明为final的类不能被其他的类继
承,避免发生这样的反模式,防止核心代码的功能被污染。注意,有的时候声明为final的类
都会有其不同的原因,比如,String类被声明为final,因为我们不希望有人对这样的核心
功能产生干扰。
这种思想同样也适用于使用默认方法的接口。通过精简的接口,你能获得最有效的组合,
因为你可以只选择你需要的实现。