Java有兩種機制可以為某個抽象提供多種實現——Interface和abstract class。
Interface 和 abstract class,除了比較明顯的區別(也就是能否提供基本實現),比較重要的區別是—— 接口的實現類可以處於類層次的任何一個位置,而抽象類的子類則受到這一限制。
Existing classes can be easily retrofitted to implement a new interface.
即,如果一個類要實現某個接口,只需要加上implements語句和方法實現。
而繼承一個抽象類則可能會破壞類層次,比如,屬於兩個不同類層次的類都想要某一個抽象類的行為時我們需要重新整理一下類層次。
Interfaces are ideal for defining mixins.
(“mixin”這一詞不知該如何翻譯,翻譯為"混合類型"顯得很僵硬。)
個人覺得這一條和第一條幾乎是說明同一個問題。
關於mixin,作者用Comparable舉例,其實現類表明自己的實例有相互比較的能力。
而抽象類不能隨意更新到現有的類中,考慮到類層次結構,為類提供某個行為的抽象時接口更為合適。
Interfaces allow the construction of nonhierarchical type frameworks.
事實上類層次結構並不是一無是處的,但這需要我們思考:是否需要組織為嚴格的層次結構。
比如,歌手和作曲家是否需要層次結構? 顯然他們沒有層次關系。
即:
public interface Singer{
AudioClip sing(Song s);
}
public interface Songwriter{
Song compose(boolean hit);
}
如果是創作型歌手,他需要同時擴展Singer和Songwriter。
幸好上面二者都是interface,於是我們可以:
public interface SingerSongwirter extends Singer, Songwriter{
AudioClip strum();
void actSensitive();
}
如果試圖使用抽象類解決這一問題,
也許我們可以將一個類的對象作為另一個類的field,
也許我們也可以將它們組織成類層次關系。
但如果類的數量越來越多,出現更多的組合,結構變得越來越臃腫(ps:稱為"combinatorial explosion")。
另外,說說接口比較明顯的"缺點",也就是不能提供任何實現。
但需要注意的是,這個特征並不能使抽象類取代接口。
比較好的方法將兩者結合起來,這種用法很常見。
比如Apache Shiro的DefaultSecurityManager的類層次(當然,Shiro還在不斷完善中...):
即,為接口中的定義提供一個抽象的骨架實現(skeletal implementation),將接口和抽象類的優點結合起來。
通常,一個抽象類為接口提供skeletal實現時存在這樣的命名規則,比如AbstractSet和Set、AbstractCollection和Collection。
如果我用這個skeletal實現,豈不是又要受類層次的困擾?
確實是這樣,但skeletal的意義並不在於靈活性。
先舉書中的代碼例子,靜態工廠方法使用skeletal類返回整型列表(ps:過度使用自動裝拆箱...):
public class IntArrays {
static List<Integer> intArrayAsList(final int[] a) {
if (a == null)
throw new NullPointerException();
return new AbstractList<Integer>() {
public Integer get(int i) {
return a[i];
}
@Override
public Integer set(int i, Integer val) {
int oldVal = a[i];
a[i] = val;
return oldVal;
}
public int size() {
return a.length;
}
};
}
}
另外再舉個Apache Shiro中的例子,在org.apache.shiro.realm.Realm接口中有這麼一段說明:
Most users will not implement the Realm interface directly, but will extend one of the subclasses, {@link org.apache.shiro.realm.AuthenticatingRealm AuthenticatingRealm} or {@link org.apache.shiro.realm.AuthorizingRealm}, greatly reducing the effort requird to implement a Realm from scratch.
即,直接實現某個接口是個繁瑣的工作。我們更建議使用其子類(當然,並不是必須),比如:
org.apache.shiro.realm.CachingRealm CachingRealm
org.apache.shiro.realm.AuthenticatingRealm AuthenticatingRealm
org.apache.shiro.realm.AuthorizingRealm
當然,也有簡單實現類(simple implementation),比如:
org.apache.shiro.authc.pam.ModularRealmAuthenticator
下面是書中提供的編寫skeletal的例子:
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();
}
}
相對於提供一個實現類,編寫一個skeletal實現有一些限制。
首先必須了解接口中哪些是最基本的行為,並將其實現留給子類實現,skeletal則負責接口中的其他方法(或者一個都不實現)或者其特征相關的實現。
另外,skeletal類的價值在於繼承,稍不注意就可能因為繼承而破壞封裝性。
為繼承而設計的類還需要提供相應的文檔,比如哪些方法是self-use、類實現了Serializable或者Clonnable等...
除了可以提供基本實現這一"優勢",抽象類還有一個優勢就是:抽象類的變化比接口的變化更容易。
即,在後續版本中為抽象類增加方法比接口增加方法更容易。
在不破壞實現類的情況下,在一個公有接口中增加方法,這是不可能的(即便下面是skeletal類,但一直detect下去總會有實現類存在)。
因此,設計接口是個技術活,一旦定下來就別想再變了。
抽象類和接口之間的選擇,某種角度上可以說是易擴展性和靈活性之間的選擇。