接口优于抽象类

接口和抽象类是Java语言中的两种多态实现机制。两者最明显的区别在于:抽象类允许其本身直接实现某些方法,而接口则必须保证所有方法都是抽象的。一个更重要的区别是抽象类的使用依赖于特定的继承链结构,而接口则没有这种限制,由于Java不允许多继承,抽象类的这一特点严重限制了它们的使用。

已有类可以很容易被改造为新接口的实现,只需声明实现某接口,并添加所需方法即可。比如当Comparable接口被引入时,大量的已有Java标准类被改造为它的实现。对于抽象类,则没有这样的便利。这是因为,想把两个已有类定义为同一个新抽象类的子类,必须把这个抽象类放在两条继承链的交点位置以上,这需要对两条继承链中间部分的所有类做出同样的改造:继承新抽象类,提供抽象方法的实现。很明显,这样做开销巨大,而且很可能破坏继承逻辑。

接口是实现"混入"(mixin)的理想方式。mixin实际上应该写成mix-in,是指把新功能"混入"到一个类已有的主要功能之中。比如Comparable就是一个"混入"接口,因为"可比较"对于大多数类来说,并不是一个核心功能,这一接口把一个可选功能"混入"了实现它的类中。抽象类无法做到这样的mixin,因为Java不允许多继承,一个类无法同时继承提供其主要功能和mixin功能的两个父类,而且mixin父类在继承链上也不可能有符合逻辑的位置。

用接口可以建立非层级结构的类型框架。比如歌手和作曲人都有它们自己的行为:

public interface Singer {
	AudioClip sing(Song s);
}

public interface Songwriter {
	Song compose(boolean hit);
}

现实中,经常会有歌手同时身兼作曲人,由于采用了接口来定义这两种类型,所以很容易让一个类同时实现上述两个接口来创建类型"歌手-作曲人"。甚至可以专门定义一个"歌手-作曲人"接口,并声明更多的特有行为:

public interface SingerSongwriter extends Singer, Songwriter {
	AudioClip strum();
	void actSensitive();
}

如果使用抽象类,这样的灵活性是不可想象的。由于多重继承被禁止,类似上面的多类型组合必须被以抽象类的形式预先定义出来,如果需要组合的类型多于2个,就可能需要预定义所有可能的组合(2^n个)。

通过item 16中提到的wrapper设计模式,接口具有安全而强大的功能扩展能力。

尽管接口本身不能含有任何方法的实现,开发者仍然可以通过骨干实现(skeletal implementation),结合接口和抽象类的优点,来简化用户的开发。约定俗成的,骨干实现也被称为抽象接口。比如Collections框架内,每个主要的接口都有一个骨干实现:AbstractCollection,AbstractSet,AbstractList和
AbstractMap。设计良好的骨干实现能极大地简化客户代码,比如下面的代码即可全功能实现一种List:

// Concrete implementation built atop skeletal implementation
static List intArrayAsList(final int[] a) {
	if (a == null)
		throw new NullPointerException();
	return new AbstractList() {
		public Integer get(int i) {
			return a[i]; // Autoboxing (Item 5)
		}

		@Override public Integer set(int i, Integer val) {
			int oldVal = a[i];
			a[i] = val; // Auto-unboxing
			return oldVal; // Autoboxing
		}

		public int size() {
			return a.length;
		}
	};
}

把这种骨干实现应用于item 16中提到的转发方法(forwarding methods),可以构建出类似于多重继承的行为。

下面是一个骨干实现的例子:

// Skeletal Implementation
public abstract class AbstractMapEntry<K,V>
	implements Map.Entry<K,V> {
	
	// Primitive operations
	public abstract K getKey();
	
	public abstract V getValue();
	
	// Entries in modifiable maps must override this method
	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<?,?> arg = (Map.Entry) o;
		return equals(getKey(), arg.getKey()) &&
					equals(getValue(), arg.getValue());
	}

	private static boolean equals(Object o1, Object o2) {
		return o1 == null ? o2 == null : o1.equals(o2);
	}

	// Implements the general contract of Map.Entry.hashCode
	@Override public int hashCode() {
		return hashCode(getKey()) ^ hashCode(getValue());
	}

	private static int hashCode(Object obj) {
		return obj == null ? 0 : obj.hashCode();
	}
}

由于骨干实现是被设计为专用于继承的,所以必须遵循item 17中提到的原则。

抽象类相对于接口,也有一个优点,那就是抽象类更容易扩展。比如一个抽象类发布以后,api开发者可以很方便地添加任何具体方法,子类会自然继承它们。这对接口来说是不可能的,任何对接口的修改都会破坏用户代码,迫使用户做出相应调整。