爲何說Java中要慎重使用繼承

JAVA中使用到繼承就會有兩個沒法迴避的缺點:函數

打破了封裝性,迫使開發者去了解超類的實現細節,子類和超類耦合。超類更新後可能會致使錯誤。繼承打破了封裝性學習

關於這一點,下面是一個詳細的例子(來源於Effective Java第16條)測試

 

這裏自定義了一個HashSet,重寫了兩個方法,它和超類惟一的區別是加入了一個計數器,用來統計添加過多少個元素。設計

寫一個測試來測試這個新增的功能是否工做:指針

 

運行後會發現,加入了3個元素以後,計數器輸出的值是6。對象

進入到超類中的addAll()方法就會發現出錯的緣由:它內部調用的是add()方法。因此在這個測試裏,進入子類的addAll()方法時,數器加3,而後調用超類的addAll(),超類的addAll()又會調用子類的add()三次,這時計數器又會再加三。blog

問題的根源繼承

將這種狀況抽象一下,能夠發現出錯是由於超類的可覆蓋的方法存在自用性(即超類裏可覆蓋的方法調用了別的可覆蓋的方法),這時候若是子類覆蓋了其中的一些方法,就可能致使錯誤。開發

 

好比上圖這種狀況,Father類裏有可覆蓋的方法A和方法B,而且A調用了B。子類Son重寫了方法B,這時候若是子類調用繼承來的方法A,那麼方法A調用的就再也不是Father.B(),而是子類中的方法Son.B()。若是程序的正確性依賴於Father.B()中的一些操做,而Son.B()重寫了這些操做,那麼就極可能致使錯誤產生。文檔

關鍵在於,子類的寫法極可能從表面上看來沒有問題,可是卻會出錯,這就迫使開發者去了解超類的實現細節,從而打破了面向對象的封裝性,由於封裝性是要求隱藏實現細節的。更危險的是,錯誤不必定能輕易地被測出來,若是開發者不瞭解超類的實現細節就進行重寫,那麼可能就埋下了隱患。

超類更新時可能產生錯誤

這一點比較好理解,主要有如下幾種可能:

超類更改了已有方法的簽名。會致使編譯錯誤。超類新增了方法:和子類已有方法的簽名相同但返回類型不一樣,會致使編譯錯誤。和子類的已有方法簽名相同,會致使子類無心中複寫,回到了第一種狀況。

和子類無衝突,但可能會影響程序的正確性。好比子類中元素加入集合必需要知足特定條件,這時候若是超類加入了一個無需檢測就能夠直接將元素插入的方法,程序的正確性就受到了威脅。

設計可繼承的類設計能夠用來繼承的類時,應該注意:

對於存在自用性的可覆蓋方法,應該用文檔精確描述調用細節。儘量少的暴露受保護成員,不然會暴露太多實現細節。構造器不該該調用任何可覆蓋的方法。詳細解釋下第三點。它實際上和 繼承打破了封裝性 裏討論的問題很類似,假設有如下代碼:

 

 

上述代碼在運行測試時就會拋出NullPointerException :

 

由於超類的構造函數會在子類的構造函數以前先運行,這裏超類的構造函數對someMethod()有依賴,同時someMethod()被重寫,因此超類的構造函數裏調用到的將是Son.someMethod(),而這時候子類還沒被初始化,因而在運行到date.getTime()時便拋出了空指針異常。

所以,若是在超類的構造函數裏對可覆蓋的方法有依賴,那麼在繼承時就可能會出錯。

結論慎重使用繼承,複合優先於繼承。

使用繼承時重寫超類中存在自用性的可覆蓋方法可能會出錯,即便不進行重寫,超類更新時也可能會引入錯誤。

若是使用繼承和複合皆可,那麼優先使用複合,上述關於繼承的缺點均可以用複合來避免。

若是要使用繼承,那麼應該精心設計超類,並提供詳細文檔。

若是你在Java的開發工做中遇到困難,或者學習遇到瓶頸,歡迎加入咱們的Java總羣:309603235,羣內有Java的技術大牛,歡迎菜鳥,老鳥入坑。

相關文章
相關標籤/搜索