這篇文章的主題並不是鼓勵不使用繼承,而是僅從使用繼承帶來的問題出發,討論繼承機制不太好的地方,從而在使用時慎重選擇,避開可能遇到的坑。ide
JAVA中使用到繼承就會有兩個沒法迴避的缺點:函數
關於這一點,下面是一個詳細的例子(來源於Effective Java第16條)測試
public class MyHashSet<E> extends HashSet<E> { private int addCount = 0; public int getAddCount() { return addCount; } @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); } }
這裏自定義了一個HashSet
,重寫了兩個方法,它和超類惟一的區別是加入了一個計數器,用來統計添加過多少個元素。this
寫一個測試來測試這個新增的功能是否工做:設計
public class MyHashSetTest { private MyHashSet<Integer> myHashSet = new MyHashSet<Integer>(); @Test public void test() { myHashSet.addAll(Arrays.asList(1,2,3)); System.out.println(myHashSet.getAddCount()); } }
運行後會發現,加入了3個元素以後,計數器輸出的值是6。指針
進入到超類中的addAll()
方法就會發現出錯的緣由:它內部調用的是add()
方法。因此在這個測試裏,進入子類的addAll()
方法時,數器加3,而後調用超類的addAll()
,超類的addAll()
又會調用子類的add()
三次,這時計數器又會再加三。code
將這種狀況抽象一下,能夠發現出錯是由於超類的可覆蓋的方法存在自用性(即超類裏可覆蓋的方法調用了別的可覆蓋的方法),這時候若是子類覆蓋了其中的一些方法,就可能致使錯誤。對象
好比上圖這種狀況,Father
類裏有可覆蓋的方法A
和方法B
,而且A
調用了B
。子類Son
重寫了方法B
,這時候若是子類調用繼承來的方法A
,那麼方法A
調用的就再也不是Father.B()
,而是子類中的方法Son.B()
。若是程序的正確性依賴於Father.B()
中的一些操做,而Son.B()
重寫了這些操做,那麼就極可能致使錯誤產生。blog
關鍵在於,子類的寫法極可能從表面上看來沒有問題,可是卻會出錯,這就迫使開發者去了解超類的實現細節,從而打破了面向對象的封裝性,由於封裝性是要求隱藏實現細節的。更危險的是,錯誤不必定能輕易地被測出來,若是開發者不瞭解超類的實現細節就進行重寫,那麼可能就埋下了隱患。繼承
這一點比較好理解,主要有如下幾種可能:
設計能夠用來繼承的類時,應該注意:
詳細解釋下第三點。它實際上和 繼承打破了封裝性 裏討論的問題很類似,假設有如下代碼:
public class Father { public Father() { someMethod(); } public void someMethod() { } }
public class Son extends Father { private Date date; public Son() { this.date = new Date(); } @Override public void someMethod() { System.out.println("Time = " + date.getTime()); } }
上述代碼在運行測試時就會拋出NullPointerException
:
public class SonTest { private Son son = new Son(); @Test public void test() { son.someMethod(); } }
由於超類的構造函數會在子類的構造函數以前先運行,這裏超類的構造函數對someMethod()
有依賴,同時someMethod()
被重寫,因此超類的構造函數裏調用到的將是Son.someMethod()
,而這時候子類還沒被初始化,因而在運行到date.getTime()
時便拋出了空指針異常。
所以,若是在超類的構造函數裏對可覆蓋的方法有依賴,那麼在繼承時就可能會出錯。
繼承有不少優勢,但使用繼承時應該慎重並多加考慮。一樣用來實現代碼複用的還有複合,若是使用繼承和複合皆可(這是前提),那麼應該優先使用複合,由於複合能夠保持超類對實現細節的屏蔽,上述關於繼承的缺點均可以用複合來避免。這也是所謂的複合優先於繼承。
若是使用繼承,那麼應該留意重寫超類中存在自用性的可覆蓋方法可能會出錯,即便不進行重寫,超類更新時也可能會引入錯誤。同時也應該精心設計超類,對任何相互調用的可覆蓋方法提供詳細文檔。