×

消息

EU e-Privacy Directive

This website uses cookies to manage authentication, navigation, and other functions. By using our website, you agree that we can place these types of cookies on your device.

View e-Privacy Directive Documents

You have declined cookies. This decision can be reversed.

接口优于抽象类

接口和抽象类是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开发者可以很方便地添加任何具体方法,子类会自然继承它们。这对接口来说是不可能的,任何对接口的修改都会破坏用户代码,迫使用户做出相应调整。

提交评论


安全码
刷新