第十六條 複合優先於繼承
封裝、繼承、多型是java的三大特徵,而封裝和繼承有點對立。封裝是指把一個功能封裝到方法裡,隱蔽細節,直接暴露給他人呼叫;繼承則是程式碼重用的有效手段,把共同的程式碼抽取到一個基類裡 ,子類去繼承,這樣能減輕工作量和維護量,但如果使用不當,程式也會變得很脆弱。好的寫法可以通過繼承來建立一套從上到下的骨架型別的類,差勁的就很明顯的打破了封裝性,子類會及其依賴 於父類的功能,一旦修改稍有不慎,很容易出問題。本章有個例子,現在對例子進行解析
public class InstrumentedHashSet<E> extends HashSet<E> { // The number of attempted element insertions private int addCount = 0;
public InstrumentedHashSet() { }
public InstrumentedHashSet(int initCap, float loadFactor) { super(initCap, loadFactor); }
@Override public boolean add(E e) { addCount++; return super.add(e); }
@Override public boolean addAll(Collection<? extends E> c) { addCount += c.size(); return super.addAll(c); }
public int getAddCount() { return addCount; } }
InstrumentedHashSet<String> s = new InstrumentedHashSet<>(); s.add("1"); s.add("2"); s.add("3");
int size = s.getAddCount(); 此時s的值為3,正確;但是,如果呼叫另外一個方法
InstrumentedHashSet<String> s = new InstrumentedHashSet<>(); s.addAll(Arrays.asList("a", "b", "c"));
int size = s.getAddCount(); 此時s的值為6,不正確;命名穿進去三個值,為什麼變成6個呢? 我們點進去addAll()方法,super()原始碼為 public boolean addAll(Collection<? extends E> c) { boolean modified = false; for (E e : c) if (add(e)) modified = true; return modified; } 裡面回去呼叫add(e)方法,InstrumentedHashSet重寫了add()方法,父類的裡的方法互相呼叫時,如果子類重寫了,會執行子類中的方法,我們子類的方法是addCount++;return super.add(e); 計數 的num增加了三次,然後呼叫父類的add()方法,所有3被算了兩次,集合元素的計數為6。我們可以重寫addAll()方法,不在這裡面呼叫super方法,自己去實現邏輯,但這樣做會很麻煩,並且map的正 確性要依賴於HashSet的方法;如果我們把子類的add()方法中addCount++;呢,這樣呼叫addAll()是沒問題,但單獨呼叫add()方法則不會計數;還有一個方法呢,把子類addAll()方法中的addCount += c.size();去掉,但是這樣的耦合依賴關係太嚴重了,如果某天,java把HashSet的原始碼做出修改,不依賴於add()方法,我們怎麼辦?上面問題之所以是問題,是因為一些原始碼不固定,會因為效率或其 他原因而被修改,我們使用時不能完全掌握或需要消耗比較大的代價才能掌握其中的細節,然後根據細節去實現子類自己的邏輯。這樣子類就依靠父類了,耦合度提高,容易出問題,沒有做到自己管 理自己,不論父類怎麼變化,只要符合功能的規範,子類就一直能保持正確性。如果要做到這一步,僅僅靠繼承,重寫子類的方法,是不夠的。所以,複合登場了!複合不用去擴充套件父類,而是類似於 代理,在類中增加一個私有成員變數,通過轉發來實現互動,不在乎類的細節,能完美的實現功能。
public class InstrumentedSet<E> extends ForwardingSet<E> { private int addCount = 0;
public InstrumentedSet(Set<E> s) { super(s); }
@Override public boolean add(E e) { addCount++; return super.add(e); }
@Override public boolean addAll(Collection<? extends E> c) { addCount += c.size(); return super.addAll(c); }
public int getAddCount() { return addCount; } }
public class ForwardingSet<E> implements Set<E> { private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s = s; }
public boolean isEmpty() { return s.isEmpty(); }
public int size() { return s.size(); }
public Iterator<E> iterator() { return s.iterator(); }
public boolean add(E e) { return s.add(e); }
public boolean addAll(Collection<? extends E> c) { return s.addAll(c); }
... }
Set<String> originSet = new HashSet<>(); ForwardingSet<String> forwardingSet = new ForwardingSet<>(originSet); InstrumentedSet<String> set = new InstrumentedSet<>(forwardingSet); set.add("1"); set.addAll(Arrays.asList("a", "b", "c"));
這個例子中,一個類包裹著另外一個。ForwardingSet類實現了Set<E> 介面,這樣就能保證Set集合中的方法都被重寫;不實現Set<E>介面也行,只要保證把Set的方法寫全就可以了,但一般只要稍微 不留意,就會出錯,implements Set<E> 直接實現介面,安全又省力,implements Set<E>的作用僅僅是這個,並非複合的私有域非得實現一個介面,不要理解錯了,筆者剛開始就理解偏了。 InstrumentedSet繼承ForwardingSet方法,需要用Set的哪個方法,就重寫一下,添加個addCount計數就行了。我們呼叫 InstrumentedSet 的 add()方法,會計數加一,然後呼叫ForwardingSet的add ()方法,注意,此時起作用的是s.add(e);方法,s是什麼呢,就是一開始建立的originSet ,這個物件傳進了ForwardingSet的構造裡,同理,addAll方法也是一樣,呼叫了s.addAll(c);此時, s.addAll(c)方法會執行父類的新增方法,呼叫add()方法,但這時候的add()方法是 originSet 的add(),非是InstrumentedSet的add(),因此不會讓計數額外增加。這樣,不論HashSet內部邏輯怎麼 變換,都對我們這個類沒影響。這就是複合的好處。
複合也有缺陷,因為是要明確知道包裝的是什麼,所以對於回撥框架,因為回撥框架是把自身物件引用傳遞給其他物件使用,因此不確定回調出來的是什麼,包裝類就不好用了。繼承功能很強大,復 合也很巧妙,如例子中的 Properties 和 Stack,使用複合效果會更好,大家可以自己寫個例子,體會一下。