1. 程式人生 > >第18條:接口優先抽象類

第18條:接口優先抽象類

multiple ace .com cts 調用 基本 允許 事物 target

Java程序設計語言提供了兩種機制,可以用來定義允許多個實現的類型:接口和抽象類。這兩種機制之間最明顯的區別在於:

1、抽象類允許包含某些方法的實現,但是接口不允許

2、為了實現由抽象類定義的類型,類必須為抽象類的一個子類

任何一個類,只要它定義了所有必要的方法,並且遵守通用約定,它就被允許實現一個接口,而不管這個類是處於類層次(class hierarchy)的哪個位置。因為Java是單繼承的,所以抽象類作為類型定義受到了極大的限制。

現有的類可以很容易被更新,以實現新的接口。如果這些方法尚不存在,你所需要做的就是增加必要的方法,然後在類的聲明中增加一個implements子句。

接口是定義mixin(混合類型)的理想選擇

。不嚴格的講,mixin是指這樣的類型:類除了實現它的“基本類型(primary type)”之外,還可以實現這個mixin類型,以表達它提供了某些可供選擇的行為。例如,Comparable是一個mixin接口,它允許類表明它的實例可以與其它的可相互比較的對象進行排序。這樣的接口之所以被稱為mixin,是因為它允許任選的功能可被混合到類型的主要功能中。抽象類不能被用於定義為mixin,同樣也是因為它們不可能被更新到現有的類中:類不可能有一個以上的父類,類層次結構中也沒有適當的地方來插入mixin。

接口允許我們構造非層次結構的類型框架。類型層次對於組織某些事物是非常合適的,但是其他有些事物並不能被整齊的組織成一個嚴格的層次結構。例如,假設我們有一個接口代表一個singer(歌唱家),另一個接口代表一個songwriter(作曲家):

1 public interface Singer {
2     AudioClip singer(Song s);
3 }
4 public interface Songwriter {
5     Song compose(boolean hit);
6 }

在現實生活中,有些歌唱家本身也是作曲家。因為我們使用了接口而不是抽象類來定義這些類型,所以對單個類而言,它同時實現Singer和Songwriter是完全允許的。實際上,我們可以定義第三個接口,它同時擴展了Singer和Songwriter,並添加了一些適合於這種組合的新方法:

1 public interface
SingerSongwriter extends Singer, Songwrite { 2 AudioClip strum(); 3 void actSensitive(); 4 }

你並不總是需要這種靈活性,但是一旦你這樣做了,接口能幫你解決大問題。另外一種做法是編寫一個臃腫(bloated)的類層次,對於每一種要被支持的屬性組合,都包含一個單獨的類。如果在整個類型系統中有n個屬性,那麽就必須支持2^n種可能的組合。這種現象被稱為“組合爆炸(combinatorial explosion)”。類層次臃腫會導致類也臃腫,這種類包含許多方法,並且這些方法只是在參數的類型上有所不同而已,因為類層次中沒有任何類型體現了公共的行為特征。

通過第16條中介紹的包裝類(wrapper class)模式,接口使得安全的增強類的功能稱為可能。如果使用抽象類來定義類型,那麽程序員除了使用繼承的手段來增加安全性,沒有其它的選擇。這樣得到的類與包裝類相比,功能更差,也更加脆弱。

雖然接口不允許包含方法的實現,但是,使用 接口來定義類型並不妨礙你為程序員提供實現上的幫助。通過對你導出的每個重要接口都提供一個抽象的骨架實現(skeletal implementation)類,把接口和抽象類的優點結合起來。接口的作用仍是定義類型,但是骨架實現類接管了所有與接口實現相關的工作。

按照慣例,骨架實現被稱為AbstractInterface是指所實現的接口的名字。例如,Colletions Framework為每個重要的集合接口都提供了一個骨架實現,包括AbstractCollection、AbstractSet、AbstractList和AbstractMap。將它們稱作SkeletalCollection、SkeletalSet、SkeletalList和SkeletalMap也是有道理的,但是現在Abstract的用法已經根深蒂固。

如果設計得當,骨架實現可以使程序員很容易地提供他們自己的接口實現。例如,下面是一個靜態工廠方法,它包含一個完整的、功能齊全的List實現:

 1 static List<Integer> intArrayList(final int[] a){
 2     if (a == null)
 3         throw new NullPointerException();
 4     
 5     return new AbstractList<Integer>() {
 6         @Override
 7         public Integer get(int index) {
 8             return a[index];
 9         }
10 
11         @Override
12         public Integer set(int index, Integer element) {
13             int oldElement = a[index];
14             a[index] = element;
15             return oldElement;
16         }
17 
18         @Override
19         public int size() {
20             return a.length;
21         }
22     };
23 }

當你考慮一個List實現應該為你完成哪些工作的時候,可以看出,這個例子充分演示了骨架實現的強大功能。順便提一下,這個例子是個Adapter,它允許將int數組看做Integer實例的列表。由於在int值和Integer實例之間來回轉換需要開銷,它的性能不會很好。註意,這個例子中只提供一個靜態工廠,並且這個類還是不可被訪問的匿名類(anonymous class),它被隱藏在靜態工廠的內部。

骨架實現的美妙之處在於,它們為抽象類提供了實現上的幫助,但又不強加“抽象類被用作類型定義時”所特有的嚴格限制。對於接口的大多數實現來講,擴展骨架實現類是很顯然的選擇,但並不是必須的。如果預置的類無法擴展骨架實現類,這個類始終可以手工實現這個接口。此外,骨架實現類仍然能夠有助於接口的實現。實現了這個接口的類可以把對於接口方法的調用,轉發到一個內部私有類的實例上,這個內部私有類擴展了骨架實現類。這種方法被稱為模擬多重繼承(simulated multiple inheritance),他與第16條中討論的包裝類模式密切相關。這項技術具有多重繼承的絕大多數有點,同時又避免了相應的缺陷。

編寫骨架實現類相對比較簡單,只是有點單調乏味。首先,必須認真研究接口,並確定哪些方法是最為基本的(primitive),其它的方法則可以根據他們來實現。這些基本方法將成為骨架實現類中的抽象方法。然後,必須為接口中所有的其它方法提供具體的實現。例如,下面是Map.Entry接口的骨架實現類:

 1 public abstract class AbstractMapEntry<K,V> implements Map.Entry<K, V>{
 2     //基本的方法
 3     public abstract K getKey();
 4     public abstract V getValue();
 5     //必須重寫的方法
 6     public V setValue(V value) {
 7         throw new UnsupportedOperationException();
 8     }
 9     @Override
10     public boolean equals(Object obj) {
11         if(obj == this)
12             return true;
13         if(! (obj instanceof Map.Entry))
14             return false;
15         Map.Entry<?, ?> arg = (Map.Entry) obj;
16         return equals(getKey(), arg.getKey()) && 
17                 equals(getValue(), arg.getValue());
18     }
19     private static boolean equals(Object o1, Object o2) {
20         return o1 == null ? o2 == null : o1.equals(o2);
21     }
22     
23     @Override
24     public int hashCode() {
25         return hashCode(getKey()) ^ hashCode(getValue());
26     }
27     private static int hashCode(Object obj) {
28         return obj == null ? 0 : obj.hashCode();
29     }
30 }

骨架實現上有個小小的不同,就是簡單實現(simple implementation)、AbstractMap.SimpleEntry就是個例子。簡單實現就像個骨架實現,這是因為它實現了接口,並且是為了繼承而設計的,但是區別在於它不是抽象的:它是最簡單的可能的有效實現。你可以原封不動的使用,也可以看情況將它子類化

使用抽象類來定義允許多個實現的類型,與使用接口相比有一個明顯的優勢:抽象類的演變比接口的演變要容易的多。如果在後續的發行版本中,你希望在抽象類中增加新的方法,始終可以增加具體方法,它包含合理的默認實現。然後,該抽象類的所有現有實現都將提供這個新的方法。對於接口,這樣做是行不通的

一般來說,要想在公有接口中增加方法,而不破壞實現這個接口的所有現有的類,這個是不可能的。之前實現該接口的類將會漏掉新增的方法,並且無法通過編譯。在為接口增加新方法的同時,也為骨架實現類增加同樣的新方法,這樣可以在一定程度上減小由此帶來的破壞,但是,這樣做並沒有真正解決問題。所有不從骨架實現類繼承的接口仍然會遭到破壞

因此,設計公有的接口要非常的謹慎。接口一旦被公開發行,並且已經被廣泛實現,在想改變這個接口幾乎是不可能的。

簡而言之,接口通常是定義允許多個實現的類型的最佳途徑。這條規則有個例外,即當演變的容易性比靈活性和功能更為重要的時候。在這種情況下,應該使用抽象類來定義類型,但是前提是必須理解並且可以接受這些局限性。如果你導出了一個重要的接口,就應該堅決考慮同時提供骨架實現類。最後,應該盡可能謹慎的設計所有的公有接口,並通過編寫多個實現來對它們進行全面測試。

第18條:接口優先抽象類