繼承是實現代碼重用的方法之一,但使用不當則會導致諸多問題。
繼承會破壞封裝性,對一個具體類進行跨包訪問級別的繼承很危險。即,子類依賴父類的實現細節。如果父類的實現細節發生變化,子類則可能遭到破壞。
舉個例子,擴展HashSet,記錄HashSet實例創建以來一共進行了多少次添加元素的操作。HashSet有兩個添加元素的方法——add(E e)和addAll(Collection<? extends E> c)。那就覆蓋這兩個方法,在添加操作執行前記錄次數:
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; }}
測試一下,通過addAll方法添加3個元素:
public static void main(String[] args) { InstrumentedHashSet<String> s = new InstrumentedHashSet<String>(); s.addAll(Arrays.asList("Snap", "Crackle", "Pop")); System.out.println(s.getAddCount());}
結果是: wKioL1PLrWjBf_fsAADIJ3WV64w978.jpg
導致這種結果的原因很簡單。參考AbstractCollection中的add(E e)和addAll(Collection<? extends E> c),add(E e)中只有一段throw new UnsupportedOperationException();而addAll(Collection<? extends E> c)的文檔注釋中有這么一段話:
* <p>Note that this implementation will throw an* <tt>UnsupportedOperationException</tt> unless <tt>add</tt> is* overridden (assuming the specified collection is non-empty).
解決這個問題的方法很簡單,只需要去掉覆蓋的addAll方法即可。但這樣卻不能解決根本問題,即HashSet的addAll方法不保證在以后的發行版本中不發生變化。即,子類實現依賴父類實現,父類發生變化時子類遭到破壞。也許我們可以覆蓋父類方法重新實現,雖然解決問題,但這樣費力不討好,毫無意義。
另外,父類增加或者移除方法也會對子類產生影響。舉個例子,子類擴展了某個集合類,覆蓋了所有添加元素的方法,在添加元素之前對元素進行檢查,讓所有元素滿足某個條件。如果在后來的版本中,父類增加了新的添加元素的方法,而子類沒有覆蓋該方法,導致非法元素添加到集合中。
反之,也有可能出現這種情況。即便父類的實現沒有問題,但也可以因為子類實現不當而破壞父類的約束。比如,父類恰好增加了和子類相同簽名和返回類型的方法。
于是,為了應對這些情況,可以使用復合模式(composition)代替繼承。即,在一個forwarding class中增加一個private field引用現有類的實例,forwarding class中的方法對應現有類的方法。代碼如下:
import java.util.Collection;import java.util.Iterator;import java.util.Set;public class ForwardingSet<E> implements Set<E> { private final Set<E> s; public ForwardingSet(Set<E> s) { this.s = s; } public void clear() { s.clear(); } public boolean contains(Object o) { return s.contains(o); } 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 remove(Object o) { return s.remove(o); } public boolean containsAll(Collection<?> c) { return s.containsAll(c); } public boolean addAll(Collection<? extends E> c) { return s.addAll(c); } public boolean removeAll(Collection<?> c) { return s.removeAll(c); } public boolean retainAll(Collection<?> c) { return s.retainAll(c); } public Object[] toArray() { return s.toArray(); } public <T> T[] toArray(T[] a) { return s.toArray(a); } @Override public boolean equals(Object o) { return s.equals(o); } @Override public int hashCode() { return s.hashCode(); } @Override public String toString() { return s.toString(); }}
使用時直接繼承forwarding class:
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; }}
forwarding class通過Set接口提供了相應方法。這種設計也有其靈活性,繼承只能選擇Set的某個特定實現,但使用復合我們可以選擇任何接口實現。比如:
public static void main(String[] args) { InstrumentedSet<String> s = new InstrumentedSet<String>( new HashSet<String>()); s.addAll(Arrays.asList("Snap", "Crackle", "Pop")); System.out.println(s.getAddCount());}
如何從繼承和復合之間做出選擇?比較抽象的說法是,只有子類和父類確實存在"is-a"關系的時候使用繼承,否則使用復合。或者比較實際點的說法是,如果TypeB只需要TypeA的部分行為,則考慮使用復合。
新聞熱點
疑難解答