Effective Java 第三版读书笔记——条款 20:接口优于抽象类
Java 有两种机制定义多实现类型:接口和抽象类。由于在 Java 8 中引入了接口的默认方法,因此这两种机制都允许为某些实例方法提供实现。一个主要的区别是要实现由抽象类定义的类型,该类必须是抽象类的子类。因为 Java 只允许单一继承,所以抽象类的这种局限严格限制了它作为类型定义的使用。任何定义所有必需方法并服从通用约定的类都可以实现一个接口,而不管类在类层次结构中的位置。
现有的类可以很容易地进行改进来实现一个新的接口。你只需添加所需的方法(如果尚不存在的话),并向类声明中添加一个 implements
子句。 例如,当 Comparable
, Iterable
, 和 Autocloseable
接口添加到 Java 平台时,很多现有类需要实现它们来加以改进。一般来说,现有的类不能改进以继承一个新的抽象类。
接口是定义混合类型(mixin)的理想选择。一般来说,mixin是一个类,除了它的“主类型”之外,还可以声明它提供了一些可选的行为。例如,Comparable
是一个类型接口,它允许一个类声明它的实例相对于其他可相互比较的对象是有序的。这样的接口被称为混合类型,因为它允许可选功能被“混合”到类型的主要功能。抽象类不能用于定义混合类,这是因为它们不能被加载到现有的类中:一个类不能有多个父类,并且在类层次结构中没有合理的位置来插入一个类型。
接口允许构建非层级类型的框架。类型层级对于组织某些事物来说是很好的,但是其他的事物肯并不是整齐地落入严格的层级结构中。例如,假设我们有一个代表歌手的接口,另一个代表作曲家的接口:
public interface Singer {
AudioClip sing(Song s);
}
public interface Songwriter {
Song compose(int chartPosition);
}
在现实生活中,一些歌手也是作曲家。因为我们使用接口而不是抽象类来定义这些类型,所以单个类实现歌手和作曲家两个接口是完全允许的。事实上,我们可以定义一个继承歌手和作曲家的第三个接口,并添加适合于这个组合的新方法:
public interface SingerSongwriter extends Singer, Songwriter {
AudioClip strum();
void actSensitive();
}
接口通过包装类(条款 18)模式确保安全的、强大的功能增强。如果使用抽象类来定义类型,那么就让程序员只能通过继承添加新功能,这样生成的类比包装类更脆弱。
你可以通过提供一个抽象的骨架实现类(abstract skeletal implementation class)将接口和抽象类的优点结合起来。接口定义了类型,可能提供了一些默认的方法,而骨架实现类在原始接口方法的顶层实现了剩余的非原始接口方法。继承骨架实现需要大部分的工作来实现一个接口,这就是模板方法设计模式。
按照惯例,骨架实现类被称为 AbstractInterface,其中 Interface 是它实现的接口的名称。例如,集合框架( Collections Framework)提供了一个框架实现以配合每个主要集合接口:AbstractCollection
,AbstractSet
,AbstractList
和AbstractMap
。如果设计得当,骨架实现(无论是单独的抽象类还是仅由接口上的默认方法组成)可以使程序员非常容易地提供他们自己的接口实现。例如,下面是一个静态工厂方法,在 AbstractList
的顶层包含一个完整的功能齐全的 List 实现:
// Concrete implementation built atop skeletal implementation
static List<Integer> intArrayAsList(int[] a) {
Objects.requireNonNull(a);
// The diamond operator is only legal here in Java 9 and later
// If you're using an earlier release, specify <Integer>
return new AbstractList<>() {
@Override public Integer get(int i) {
return a[i]; // Autoboxing (Item 6)
}
@Override public Integer set(int i, Integer val) {
int oldVal = a[i];
a[i] = val; // Auto-unboxing
return oldVal; // Autoboxing
}
@Override public int size() {
return a.length;
}
};
}
骨架实现类的优点在于,它们提供抽象类的所有实现的帮助,而不会强加抽象类作为类型定义时的严格约束。对于具有骨架实现类的接口的大多数实现者来说,继承这个类是显而易见的选择,但它不是必需的。如果一个类不能继承骨架实现,这个类可以直接实现接口。该类仍然受益于接口本身的任何默认方法。
编写一个骨架的实现是一个相对简单的过程。首先,研究接口,并确定哪些方法是基本的,其他方法可以根据它们来实现。这些基本方法是你的骨架实现类中的抽象方法。接下来,为接口中所有可以直接在基本方法之上实现的方法提供默认方法,回想一下,你可能不会为诸如 Object
类中 equals
和 hashCode
等方法提供默认方法。如果基本方法和默认方法涵盖了接口,那么就完成了,并且不需要骨架实现类。否则,编写一个声明实现接口的类,并实现所有剩下的接口方法。
作为一个简单的例子,考虑一下 Map.Entry接口
。显而易见的基本方法是 getKey
,getValue
和(可选的)setValue
。接口指定了 equals
和 hashCode
的行为,并且在基本方法中有一个 toString
的明显的实现。由于不允许为 Object 类方法提供默认实现,因此所有实现均放置在骨架实现类中:
// Skeletal implementation class
public abstract class AbstractMapEntry<K,V> implements Map.Entry<K,V> {
// Entries in a modifiable map must override this method
@Override public V setValue(V value) {
throw new UnsupportedOperationException();
}
// Implements the general contract of Map.Entry.equals
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry) o;
return Objects.equals(e.getKey(), getKey())
&& Objects.equals(e.getValue(), getValue());
}
// Implements the general contract of Map.Entry.hashCode
@Override public int hashCode() {
return Objects.hashCode(getKey())
^ Objects.hashCode(getValue());
}
@Override public String toString() {
return getKey() + "=" + getValue();
}
}
请注意,这个骨架实现不能在 Map.Entry
接口中实现,也不能作为子接口实现,因为默认方法不允许重写诸如 equals
,hashCode
和 toString
等 Object
类方法。
由于骨架实现类是为了继承而设计的,所以你应该遵循条款 19 中的所有设计和文档说明。为了简洁起见,前面的例子中省略了文档注释,但是好的文档在骨架实现中是绝对必要的,无论它是否包含 一个接口或一个单独的抽象类的默认方法。
总而言之,一个接口通常是定义一个允许多个实现的类型的最佳方式。如果你导出一个重要的接口,应该考虑提供一个骨架实现类。在可能的情况下,应该通过接口上的默认方法提供骨架实现,以便接口的所有实现者都可以使用它。也就是说,对接口的限制通常要求骨架实现类采用抽象类的形式。