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)提供了一个框架实现以配合每个主要集合接口:AbstractCollectionAbstractSetAbstractListAbstractMap。如果设计得当,骨架实现(无论是单独的抽象类还是仅由接口上的默认方法组成)可以使程序员非常容易地提供他们自己的接口实现。例如,下面是一个静态工厂方法,在 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 类中 equalshashCode 等方法提供默认方法。如果基本方法和默认方法涵盖了接口,那么就完成了,并且不需要骨架实现类。否则,编写一个声明实现接口的类,并实现所有剩下的接口方法。

作为一个简单的例子,考虑一下 Map.Entry接口。显而易见的基本方法是 getKeygetValue 和(可选的)setValue。接口指定了 equalshashCode 的行为,并且在基本方法中有一个 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 接口中实现,也不能作为子接口实现,因为默认方法不允许重写诸如 equalshashCodetoStringObject 类方法。

由于骨架实现类是为了继承而设计的,所以你应该遵循条款 19 中的所有设计和文档说明。为了简洁起见,前面的例子中省略了文档注释,但是好的文档在骨架实现中是绝对必要的,无论它是否包含 一个接口或一个单独的抽象类的默认方法。

总而言之,一个接口通常是定义一个允许多个实现的类型的最佳方式。如果你导出一个重要的接口,应该考虑提供一个骨架实现类。在可能的情况下,应该通过接口上的默认方法提供骨架实现,以便接口的所有实现者都可以使用它。也就是说,对接口的限制通常要求骨架实现类采用抽象类的形式。

posted @ 2019-01-22 10:03  LeeFire  阅读(172)  评论(0编辑  收藏  举报