先扯兩句
原本是不想扯了的,因?yàn)楹芫脹](méi)扯了也不知道該說(shuō)寫(xiě)什么,可是這里氏替換原則東西實(shí)在是太多了,我看過(guò)都快一周了,但是每次想寫(xiě)博客的時(shí)候,都寫(xiě)幾個(gè)字就扔下了,倒不是說(shuō)書(shū)中的內(nèi)容不夠詳細(xì),只是如果都是摘抄書(shū)的話(huà),這個(gè)系列的意義也就沒(méi)有了,而且從個(gè)人的角度來(lái)說(shuō),不能用自己的話(huà)說(shuō)出來(lái)的東西,都不是自己的。
還好的是,總算是東拼西湊的時(shí)間完成了這篇博客,不至于像上一個(gè)系列一樣無(wú)疾而終。堅(jiān)持是一種好習(xí)慣,希望我能保持下去,與大家共勉吧。
下面才是這次扯的目的,哈哈哈,《設(shè)計(jì)模式》——目錄,好了,閑言少敘,我們進(jìn)入正題。
一. 繼承
要說(shuō)“里氏替換原則”,就要先知道什么是“里氏替換原則”,其實(shí)就是一個(gè)姓“里”的研究出來(lái)用于類(lèi)替換的原則。

好吧,別打人,我不開(kāi)玩笑了。。。
“里氏替換原則”實(shí)際是為良好的繼承定義了一個(gè)規(guī)范,對(duì)于其定義,在書(shū)中共介紹了兩種,這里我們自然是選擇最淺顯易懂的來(lái)說(shuō):
只要父類(lèi)能出現(xiàn)的地方子類(lèi)都可以出現(xiàn),而且替換為子類(lèi)也不會(huì)產(chǎn)生任何錯(cuò)誤和異常,使用者根本不需要知道自己使用的事父類(lèi)還是子類(lèi)。但是反過(guò)來(lái)就不行了,有子類(lèi)出現(xiàn)的地方,父類(lèi)未必能適應(yīng)。
若要舉個(gè)例子其實(shí)也簡(jiǎn)單:
private void getObject(Object o){
}
private void getString(String s){
}

很顯然當(dāng)我們要Object(父類(lèi))的時(shí)候,傳遞了個(gè)String(子類(lèi))也是可以通過(guò)的,但反過(guò)來(lái),我們要String的時(shí)候,傳Object卻報(bào)錯(cuò)了。為什么呢?這就要先說(shuō)說(shuō)面向?qū)ο蟮娜筇匦灾弧^承。
繼承可以使得子類(lèi)別具有父類(lèi)別的各種屬性和方法,而不需要再次編寫(xiě)相同的代碼
以上是繼承的淺顯定義,至于繼承再詳細(xì)的我這里給個(gè)鏈接吧,有興趣的可以看看,當(dāng)然,大家也可以自己查,網(wǎng)上還是有大把的講解的
Java 繼承
書(shū)中對(duì)于java的優(yōu)點(diǎn)做了闡述,我這里也列舉一下吧,或許從這些優(yōu)點(diǎn)中,大家也能理解到究竟什么是繼承,當(dāng)然,也可能看了后更蒙(可以跳過(guò),我就是這么干的)
- 代碼共享:減少創(chuàng)建類(lèi)的工作量,每個(gè)子類(lèi)都擁有父類(lèi)的方法和屬性(爸媽?zhuān)o我打錢(qián))
- 提高代碼的重用性(兒子,你張嬸家孩子結(jié)婚,我沒(méi)時(shí)間,你去一下)
- 子類(lèi)可以形似父類(lèi),但又異于父類(lèi),“龍生龍,鳳生鳳,老鼠的孩子會(huì)打洞”是說(shuō)子擁有父的“種”,“世界上沒(méi)有兩片完全相同的葉子”是指子與父的不同(你眼睛長(zhǎng)得像你爸,鼻子像你媽?zhuān)?/li>
- 提高代碼可擴(kuò)展行,實(shí)現(xiàn)父類(lèi)的方法就可以“為所欲為”了,君不見(jiàn)很多開(kāi)源框架擴(kuò)展接口都是通過(guò)繼承父類(lèi)來(lái)完成的(爸媽給我十塊錢(qián),我要買(mǎi)手套去搬磚賺錢(qián))
- 提高產(chǎn)品或項(xiàng)目的開(kāi)放性(沒(méi)事認(rèn)個(gè)干兒子也是不錯(cuò)的選擇)
當(dāng)然,凡事都是兩面的,不可能有什么只有優(yōu)點(diǎn)沒(méi)有缺點(diǎn)事物
繼承的缺點(diǎn):
- 繼承是入侵性的。只要繼承,就必須擁有父類(lèi)的所有屬性和方法;(你老子的錢(qián)是你的,你老子的債也是你的)
- 降低代碼的靈活性。子類(lèi)必須擁有父類(lèi)的屬性和方法,讓子類(lèi)自由的世界多了些約束(看看現(xiàn)在那么多吐槽原生家庭的就應(yīng)該明白了)
- 增強(qiáng)了耦合性。當(dāng)父類(lèi)的常量、變量和方法被修改是,需要考慮子類(lèi)的修改,而且在缺乏規(guī)范的環(huán)境下,這種修改可能帶來(lái)非常糟糕的結(jié)果——大量代碼的重構(gòu)(父類(lèi)瞞著兒子出去借高利貸)
二. 里氏替換原則
剛剛被繼承一陣插科打諢,估計(jì)里氏替換原則是什么都快記不清了(反正我是忘了),那就重新看一下定義吧
只要父類(lèi)能出現(xiàn)的地方子類(lèi)都可以出現(xiàn),而且替換為子類(lèi)也不會(huì)產(chǎn)生任何錯(cuò)誤和異常,使用者根本不需要知道自己使用的事父類(lèi)還是子類(lèi)。但是反過(guò)來(lái)就不行了,有子類(lèi)出現(xiàn)的地方,父類(lèi)未必能適應(yīng)。
不難看出,里氏替換原則就是為繼承定義了一個(gè)優(yōu)化使用的規(guī)范,“一句簡(jiǎn)單的定義包含了四層含義”(當(dāng)然,這么有文化的話(huà)肯定不是我說(shuō)的)
1. 子類(lèi)必須完全實(shí)現(xiàn)父類(lèi)的方法
不用懷疑,即便是私有方法無(wú)法調(diào)用,但也是會(huì)在類(lèi)內(nèi)部執(zhí)行的,誰(shuí)讓父類(lèi)里就是這么做的。當(dāng)然這里之所以需要強(qiáng)調(diào),是為了證明為什么里氏替換原則父類(lèi)出現(xiàn)的地方,子類(lèi)都可以出現(xiàn)
public abstract class YuQianParent {
abstract void smoking();
abstract void drink();
}
public class YuQian extends YuQianParent {
@Override
void smoking() {
}
@Override
void drink() {
}
}
如果子類(lèi)不能完整的實(shí)現(xiàn)父類(lèi)的方法,或者父類(lèi)的某些方法在子類(lèi)中已經(jīng)發(fā)生“畸形”,則建議斷開(kāi)父子繼承關(guān)系,采用“依賴(lài)”、“聚集”、“組合”等關(guān)系代替繼承(依賴(lài)、聚集、組合定義見(jiàn)附錄——Java的依賴(lài)、關(guān)聯(lián)、聚合和組合)
2. 子類(lèi)可以有自己的個(gè)性
public class YuQian extends YuQianParent {
@Override
void smoking() {
}
@Override
void drink() {
}
void hotHead(){
}
}
如上,于謙在實(shí)現(xiàn)了父類(lèi)的抽煙、喝酒以外,子類(lèi)還開(kāi)發(fā)出了自己的特有技能——燙頭。當(dāng)然,肯定會(huì)有人說(shuō),這不是廢話(huà)嗎!畢竟我們?cè)陂_(kāi)發(fā)中就是這么用的。但之所以強(qiáng)調(diào)一下,是為了說(shuō)明為什么里氏替換原則規(guī)定,子類(lèi)出現(xiàn)的地方父類(lèi)卻不一定適用,無(wú)奈啊,誰(shuí)讓老一輩人沒(méi)法燙頭呢!
3. 覆蓋或?qū)崿F(xiàn)父類(lèi)的方法時(shí)輸入?yún)?shù)可以被放大
1)你是爹,我是爹?
這個(gè)部分書(shū)中舉了一個(gè)例子:
public class Father {
public Collection doSomething(HashMap map){
System.out.println("父類(lèi)被執(zhí)行...");
return map.values();
}
}
public class Son extends Father {
public Collection doSomething(Map map){
System.out.println("子類(lèi)被執(zhí)行...");
return map.values();
}
}
由于傳入的參數(shù)一個(gè)是HashMap,一個(gè)是Map,并不是同一個(gè)參數(shù),所以這里不是使用的重寫(xiě)(@Override),而是重載。
@Test
public void test() {
Father f = new Father();
HashMap map = new HashMap();
f.doSomething(map);
}
其執(zhí)行結(jié)果打印日志為
父類(lèi)被執(zhí)行了
而當(dāng)將父類(lèi)替換為子類(lèi)的時(shí)候重新運(yùn)行
@Test
public void test() {
Son s = new Son();
HashMap map = new HashMap();
s.doSomething(map);
}
其執(zhí)行結(jié)果打印日志仍然為
父類(lèi)被執(zhí)行了
原因,書(shū)中自然也給出了:
子類(lèi)的輸入?yún)?shù)類(lèi)型的范圍放大了(從HashMap變成他老子Map了),子類(lèi)代替父類(lèi)傳遞到調(diào)用者中,子類(lèi)的方法永遠(yuǎn)都不會(huì)被執(zhí)行。
換種說(shuō)法就是,小學(xué)生買(mǎi)電腦,非要買(mǎi)1W的,結(jié)果拿他爸卡一刷,存款才500,那店員肯定是不能賣(mài)的。

如果非要執(zhí)行怎么辦,小學(xué)生等到大學(xué)畢業(yè)學(xué)會(huì)了賺錢(qián)的技能,自然可以自己賺錢(qián)買(mǎi),而子類(lèi)的方法多出了其他技能,如下,多了個(gè)iCanDoIt,明確指向子類(lèi)的方法就可以了。當(dāng)然,這段是實(shí)際應(yīng)用的時(shí)候用來(lái)變通的,與本篇主旨無(wú)關(guān)。
public class Son extend Father {
public Collection doSomething (Map map, Object iCanDoIt){
System.out.println("子類(lèi)被執(zhí)行...");
return map.values();
}
}
上面的文字,雖然我打出來(lái)了,但是這個(gè)方法是提供給維護(hù)人員實(shí)在沒(méi)有其他好的方法時(shí)采用的,之所以標(biāo)為斜體,實(shí)在是這招不正。
不過(guò)前面說(shuō)的是子類(lèi)的參數(shù)設(shè)置為父類(lèi),父類(lèi)的參數(shù)設(shè)置為子類(lèi)(不說(shuō)別的,看這句話(huà)就夠亂的,這不禁讓我想起以前給我父親提意見(jiàn)的時(shí)候,他總能理直氣壯地問(wèn)我:“你是爹,我是爹?”,雖然我認(rèn)為自己的想法更成熟,但是剛剛代碼執(zhí)行的結(jié)果大家也看到了,輸出的還是父親的想法?。?/p>

2)你爹永遠(yuǎn)是你爹
但是如果我們按照正常的邏輯去考慮呢,兒子就是兒子,老子就是老子(子類(lèi)的參數(shù)設(shè)置為子類(lèi),父類(lèi)的參數(shù)設(shè)置為父類(lèi))又會(huì)怎么樣呢?
public class Father {
public Collection doSomething(Map map) {
System.out.println("父類(lèi)被執(zhí)行...");
return map.values();
}
}
public class Son extend Father{
public Collection doSomething(HashMap map) {
System.out.println("子類(lèi)被執(zhí)行...");
return map.values();
}
}
@Test
public void test() {
Father f = new Father();
HashMap map = new HashMap();
f.doSomething(map);
}
其執(zhí)行結(jié)果打印日志為
父類(lèi)被執(zhí)行了
而當(dāng)將父類(lèi)替換為子類(lèi)的時(shí)候重新運(yùn)行
@Test
public void test() {
Son s = new Son();
HashMap map = new HashMap();
s.doSomething(map);
}
其執(zhí)行結(jié)果打印日志仍然為
子類(lèi)被執(zhí)行了
輩分關(guān)系理清了,爹和兒子也都可以想干啥就干啥了,這下總可以爽了吧?。?!
很遺憾,大家正各忙各的呢,突然廚房傳來(lái)一聲“碗”打碎的巨響,這可是祖?zhèn)鱊多代的,家里唯一的一個(gè)飯碗啊,一家人可都指著它吃飯呢!這時(shí)候母親出來(lái)了,看著一地的殘骸和面面相覷的父子倆:“究竟是誰(shuí)干的!”
兒子高喊:“我爸能打碎!”
當(dāng)爸的一聽(tīng)不樂(lè)意了:“臭小子,你也能!”
當(dāng)媽的怎么辦,家里又沒(méi)安監(jiān)控,人生又不能回放,這打破碗的案子就成了懸案。過(guò)兩天,衣服被蹭上油漬成了懸案、偷吃旅行前準(zhǔn)備的蛋糕成了懸案……直到家被燒了都是一個(gè)個(gè)懸案。
當(dāng)然,現(xiàn)實(shí)中不會(huì)這么慘,畢竟一句“你是爹,我是爹?”我就秒慫了,可類(lèi)比到項(xiàng)目中呢,這個(gè)方法出了問(wèn)題,是父類(lèi)執(zhí)行錯(cuò)了,還是子類(lèi)執(zhí)行錯(cuò)了?
所以同一個(gè)方法,子類(lèi)、父類(lèi)都可以執(zhí)行的時(shí)候,維護(hù)人員就很難做到精準(zhǔn)定位問(wèn)題出在了哪里,增加了項(xiàng)目的維護(hù)難度。
子類(lèi)在沒(méi)有復(fù)寫(xiě)父類(lèi)的方法的前提下,子類(lèi)方法被執(zhí)行了,這回引起業(yè)務(wù)邏輯混亂,因?yàn)樵趯?shí)際應(yīng)用中父類(lèi)一般都是抽象類(lèi),子類(lèi)是實(shí)現(xiàn)類(lèi),你傳遞一個(gè)這樣的實(shí)現(xiàn)類(lèi)就會(huì)“歪曲”了父類(lèi)的意圖,引起一堆意想不到的業(yè)務(wù)混亂,所以子類(lèi)中方法的前置條件必須是超類(lèi)中被復(fù)寫(xiě)的方法的前置條件相同或者更寬松。
4. 覆蓋或?qū)崿F(xiàn)父類(lèi)的方法時(shí)輸出結(jié)果可以被縮小
首先請(qǐng)大家跟著我重新看一下第三條與第四條的標(biāo)題:
3.覆蓋或?qū)崿F(xiàn)父類(lèi)的方法時(shí)輸入?yún)?shù)可以被放大
4.覆蓋或?qū)崿F(xiàn)父類(lèi)的方法時(shí)輸出結(jié)果可以被縮小
如果大家已經(jīng)看出這兩條有何不同了,那我們就可以繼續(xù)向下進(jìn)行了。首先請(qǐng)?jiān)试S我摘抄書(shū)中關(guān)于第四條的定義:
父類(lèi)的一個(gè)方法的返回值是一個(gè)類(lèi)型T,子類(lèi)的相同方法(重載或復(fù)寫(xiě))的返回值為S,那么歷史替換原則就要求S必須小于等于T,也就是說(shuō),要么S和T同一個(gè)類(lèi)型,要么S是T的子類(lèi)。
當(dāng)然,憑啥他說(shuō)不行就不行,我非要試試看:

被狠狠打臉(去掉@Override一樣報(bào)錯(cuò),有這個(gè)注解的方法一定是重寫(xiě)的,但是重寫(xiě)的方法不一定一定有@Override,大家可以自己刪掉試試看)的同時(shí),當(dāng)然不能忘了看看這個(gè)報(bào)錯(cuò)究竟提示的是什么
getString()方法在Child中與getString()在Parent中沖突:試圖返回不兼容的類(lèi)型
好了,在按照規(guī)則寫(xiě)一遍

至于原理說(shuō)實(shí)話(huà)到現(xiàn)在我也沒(méi)有理解,書(shū)中也是用定義解釋定義,而且就我這不太好用的腦子分析,好像解釋的也不是第4條,而是又回去說(shuō)了第3條。
網(wǎng)上搜了一下,依然沒(méi)有找到淺顯易懂的解釋?zhuān)踔涟俣饶懿榈降臇|西都很少(也可能是搜索的關(guān)鍵字不夠關(guān)鍵)。所以只找到了一篇蟬蟬的博客Java學(xué)習(xí)筆記13---如何理解“子類(lèi)重寫(xiě)父類(lèi)方法時(shí),返回值若為類(lèi)類(lèi)型,則必須與父類(lèi)返回值類(lèi)型相同或?yàn)槠渥宇?lèi)”中有所講解,雖然我這笨腦子還是沒(méi)看懂,希望對(duì)大家有所幫助吧。
