關(guān)于為何說(shuō)JAVA中要慎重使用繼承詳解
前言
這篇文章的主題并非鼓勵(lì)不使用繼承,而是僅從使用繼承帶來(lái)的問(wèn)題出發(fā),討論繼承機(jī)制不太好的地方,從而在使用時(shí)慎重選擇,避開(kāi)可能遇到的坑。
JAVA中使用到繼承就會(huì)有兩個(gè)無(wú)法回避的缺點(diǎn):
- 打破了封裝性,子類依賴于超類的實(shí)現(xiàn)細(xì)節(jié),和超類耦合。
- 超類更新后可能會(huì)導(dǎo)致錯(cuò)誤。
繼承打破了封裝性
關(guān)于這一點(diǎn),下面是一個(gè)詳細(xì)的例子(來(lái)源于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);
}
}
這里自定義了一個(gè)HashSet,重寫了兩個(gè)方法,它和超類唯一的區(qū)別是加入了一個(gè)計(jì)數(shù)器,用來(lái)統(tǒng)計(jì)添加過(guò)多少個(gè)元素。
寫一個(gè)測(cè)試來(lái)測(cè)試這個(gè)新增的功能是否工作:
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());
}
}
運(yùn)行后會(huì)發(fā)現(xiàn),加入了3個(gè)元素之后,計(jì)數(shù)器輸出的值是6。
進(jìn)入到超類中的addAll()方法就會(huì)發(fā)現(xiàn)出錯(cuò)的原因:它內(nèi)部調(diào)用的是add()方法。所以在這個(gè)測(cè)試?yán)铮M(jìn)入子類的addAll()方法時(shí),數(shù)器加3,然后調(diào)用超類的addAll(),超類的addAll()又會(huì)調(diào)用子類的add()三次,這時(shí)計(jì)數(shù)器又會(huì)再加三。
問(wèn)題的根源
將這種情況抽象一下,可以發(fā)現(xiàn)出錯(cuò)是因?yàn)槌惖目筛采w的方法存在自用性(即超類里可覆蓋的方法調(diào)用了別的可覆蓋的方法),這時(shí)候如果子類覆蓋了其中的一些方法,就可能導(dǎo)致錯(cuò)誤。
比如上圖這種情況,F(xiàn)ather類里有可覆蓋的方法A和方法B,并且A調(diào)用了B。子類Son重寫了方法B,這時(shí)候如果子類調(diào)用繼承來(lái)的方法A,那么方法A調(diào)用的就不再是Father.B(),而是子類中的方法Son.B()。如果程序的正確性依賴于Father.B()中的一些操作,而Son.B()重寫了這些操作,那么就很可能導(dǎo)致錯(cuò)誤產(chǎn)生。
關(guān)鍵在于,子類的寫法很可能從表面上看來(lái)沒(méi)有問(wèn)題,但是卻會(huì)出錯(cuò),這就迫使開(kāi)發(fā)者去了解超類的實(shí)現(xiàn)細(xì)節(jié),從而打破了面向?qū)ο蟮姆庋b性,因?yàn)榉庋b性是要求隱藏實(shí)現(xiàn)細(xì)節(jié)的。更危險(xiǎn)的是,錯(cuò)誤不一定能輕易地被測(cè)出來(lái),如果開(kāi)發(fā)者不了解超類的實(shí)現(xiàn)細(xì)節(jié)就進(jìn)行重寫,那么可能就埋下了隱患。
超類更新時(shí)可能產(chǎn)生錯(cuò)誤
這一點(diǎn)比較好理解,主要有以下幾種可能:
1、超類更改了已有方法的簽名。會(huì)導(dǎo)致編譯錯(cuò)誤。
2、超類新增了方法:
- 和子類已有方法的簽名相同但返回類型不同,會(huì)導(dǎo)致編譯錯(cuò)誤。
- 和子類的已有方法簽名相同,會(huì)導(dǎo)致子類無(wú)意中復(fù)寫,回到了第一種情況。
- 和子類無(wú)沖突,但可能會(huì)影響程序的正確性。比如子類中元素加入集合必須要滿足特定條件,這時(shí)候如果超類加入了一個(gè)無(wú)需檢測(cè)就可以直接將元素插入的方法,程序的正確性就受到了威脅。
設(shè)計(jì)可繼承的類
設(shè)計(jì)可以用來(lái)繼承的類時(shí),應(yīng)該注意:
- 對(duì)于存在自用性的可覆蓋方法,應(yīng)該用文檔精確描述調(diào)用細(xì)節(jié)。
- 盡可能少的暴露受保護(hù)成員,否則會(huì)暴露太多實(shí)現(xiàn)細(xì)節(jié)。
- 構(gòu)造器不應(yīng)該調(diào)用任何可覆蓋的方法。
詳細(xì)解釋下第三點(diǎn)。它實(shí)際上和 繼承打破了封裝性 里討論的問(wèn)題很相似,假設(shè)有以下代碼:
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());
}
}
上述代碼在運(yùn)行測(cè)試時(shí)就會(huì)拋出NullPointerException :
public class SonTest {
private Son son = new Son();
@Test
public void test() {
son.someMethod();
}
}
因?yàn)槌惖臉?gòu)造函數(shù)會(huì)在子類的構(gòu)造函數(shù)之前先運(yùn)行,這里超類的構(gòu)造函數(shù)對(duì)someMethod()有依賴,同時(shí)someMethod()被重寫,所以超類的構(gòu)造函數(shù)里調(diào)用到的將是Son.someMethod(),而這時(shí)候子類還沒(méi)被初始化,于是在運(yùn)行到date.getTime()時(shí)便拋出了空指針異常。
因此,如果在超類的構(gòu)造函數(shù)里對(duì)可覆蓋的方法有依賴,那么在繼承時(shí)就可能會(huì)出錯(cuò)。
結(jié)論
繼承有很多優(yōu)點(diǎn),但使用繼承時(shí)應(yīng)該慎重并多加考慮。同樣用來(lái)實(shí)現(xiàn)代碼復(fù)用的還有復(fù)合,如果使用繼承和復(fù)合皆可(這是前提),那么應(yīng)該優(yōu)先使用復(fù)合,因?yàn)閺?fù)合可以保持超類對(duì)實(shí)現(xiàn)細(xì)節(jié)的屏蔽,上述關(guān)于繼承的缺點(diǎn)都可以用復(fù)合來(lái)避免。這也是所謂的復(fù)合優(yōu)先于繼承。
如果使用繼承,那么應(yīng)該留意重寫超類中存在自用性的可覆蓋方法可能會(huì)出錯(cuò),即使不進(jìn)行重寫,超類更新時(shí)也可能會(huì)引入錯(cuò)誤。同時(shí)也應(yīng)該精心設(shè)計(jì)超類,對(duì)任何相互調(diào)用的可覆蓋方法提供詳細(xì)文檔。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
基于Java利用static實(shí)現(xiàn)單例模式
這篇文章主要介紹了基于Java利用static實(shí)現(xiàn)單例模式,當(dāng)在多個(gè)線程同時(shí)觸發(fā)類的初始化過(guò)程的時(shí)候static不會(huì)被多次執(zhí)行,下面我們一起進(jìn)入文章看看具體要的原因2022-01-01
Spring源碼之事件監(jiān)聽(tīng)機(jī)制(實(shí)現(xiàn)EventListener接口方式)
這篇文章主要介紹了Spring源碼之事件監(jiān)聽(tīng)機(jī)制(實(shí)現(xiàn)EventListener接口方式),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08
SpringBoot請(qǐng)求轉(zhuǎn)發(fā)的方式小結(jié)
本文主要介紹了SpringBoot請(qǐng)求轉(zhuǎn)發(fā)的方式,一共有兩大類,一種是controller控制器轉(zhuǎn)發(fā)一種是使用HttpServletRequest進(jìn)行轉(zhuǎn)發(fā),本文就詳細(xì)的介紹一下,感興趣的可以了解一下2023-09-09
Java語(yǔ)言中flush()函數(shù)作用及使用方法詳解
這篇文章主要介紹了Java語(yǔ)言中flush函數(shù)作用及使用方法詳解,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-01-01
使用React和springboot做前后端分離項(xiàng)目的步驟方式
這篇文章主要介紹了使用React和springboot做前后端分離項(xiàng)目的步驟方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08

