1、定義
按照慣例,首先我們來看一下里氏替換原則的定義。
所有引用基類(父類)的地方必須能透明地使用其子類的物件。
通俗的說,子類可以擴充套件父類功能,但不能改變父類原有功能。
核心思想是繼承。 通過繼承,引用基類的地方就可以使用其子類的物件了。例如:
Parent parent = new Child();
重點來了,那麼如何透明地使用呢?
我們來思考個問題,子類可以改變父類的原有功能嗎?
public class Parent {
public int add(int a, int b){
return a+b;
}
}
public class Child extends Parent{
@Override
public int add(int a, int b) {
return a-b;
}
}
這樣好不好?
肯定是不好的,本來是加法卻修改成了減法,這顯然是不符合認知的。
它違背了里氏替換原則,子類改變了父類原有功能後,當我們在引用父類的地方使用其子類的時候,沒辦法透明使用add方法了。
父類中凡是已經實現好的方法,實際上是在設定一系列的規範和契約,雖然它不強制要求所有的子類必須遵從這些規範,但是如果子類對這些非抽象方法任意修改,就會對整個繼承體系造成破壞。
所以,透明使用的關鍵就是,子類不能改變父類原有功能。
2、含義
1、子類可以實現父類的抽象方法,但是不能覆蓋父類的非抽象方法。
剛才我們已經說過,子類不能改變父類的原有功能,所以子類不能覆蓋父類的非抽象方法。
子類可以實現父類的抽象方法,must be,抽象方法本來就是讓子類實現的。
package com.fanqiekt.principle.liskov.rapper;
/**
* Rapper抽象類
*
* @Author: 番茄課堂-懶人
*/
public abstract class BaseRapper {
/**
* freeStyle
*/
protected abstract void freeStyle();
/**
* 播放伴奏
*/
protected void playBeat(){
System.out.println("從樂庫中隨機播放一首伴奏:動次打次...");
}
/**
* 表演
* 播放伴奏,並進行freeStyle
*/
public void perform(){
playBeat();
freeStyle();
}
}
BaseRapper是一個抽象類,它代表著Rapper的基類。
Rapper一般的表演方式是隨機播放一首伴奏然後進行free style。
freeStyle則各有各的不同,所以將它寫成了一個抽象方法,讓子類自由發揮。
playBeat流程大多是一樣的,從樂庫中隨意播放伴奏,所以將它寫成了一個非抽象方法。
perform的流程大多也是一樣的,放伴奏,然後freestyle,也將它寫成了非抽象方法。
package com.fanqiekt.principle.liskov.rapper;
/**
* Rapper
*
* @author 番茄課堂-懶人
*/
public class Rapper extends BaseRapper {
/**
* 播放伴奏
*
* 子類覆蓋父類非抽象方法
*/
@Override
protected void playBeat() {
System.out.println("關閉麥克風");
}
/**
* 表演
*
* 子類覆蓋父類非抽象方法
*/
@Override
public void perform() {
System.out.println("跳鬼步");
}
/**
* 子類可以覆蓋父類抽象方法
*/
@Override
protected void freeStyle() {
System.out.println("藥藥切克鬧,煎餅果子來一套!");
}
}
Rapper是BaseRapper的子類,覆蓋了父類的抽象方法freeStyle。
覆蓋了父類的非抽象方法playBeat,並將邏輯更改為開啟麥克風,明顯違背了里氏替換原則。
這顯然是非常錯誤的寫法, 原因是父類行為與子類行為不一致,不可以透明的使用父類了。
播放伴奏你卻給我開啟麥克風,你確定不是在逗我?
我嘗試著將playBeat進行下修改。
/**
* 子類覆蓋父類非抽象方法
* 子類方法中呼叫super方法
*/
@Override
protected void playBeat() {
super.playBeat();
System.out.println("關閉麥克風");
}
在子類方法中呼叫super方法,這樣修改是否可以?
不可以,原因是開啟麥克風跟播放伴奏沒有任何邏輯上的關係。
透明使用子類的時候,雖然伴奏也會正常的播放,但卻在呼叫者不知情的情況下關閉了麥克風,而關閉麥克風又明顯與播放伴奏無關。
這就對於呼叫者無法做到真正的透明瞭。
同樣覆蓋了父類的非抽象方法perform,並將邏輯更改為跳舞,這要是違背了里氏替換原則的。
只跳舞不說唱的表演還叫Rapper嗎?
我嘗試著將perform進行下修改。
/**
* 表演
* freestyle + 跳舞
* 子類覆蓋父類非抽象方法
*/
@Override
public void perform() {
super.perform();
System.out.println("跳鬼步");
}
perform方法我這樣修改可以嗎?
這個倒是可以的,為什麼同樣是子類呼叫super方法,為什麼playBeat不可以,perform就可以呢?
perform是表演,跳舞是表演的一種補充,屬於表演範疇,呼叫者可以透明地呼叫perform方法。
安靜的freestyle還是手舞足蹈的freestyle,對於呼叫者來講,都屬於freestyle表演。
2、子類中可以增加自己特有的方法。
繼承一個很重要的特點:子類繼承父類後可以新增方法。
/**
* 跳舞
* 子類中增加特有的方法
*/
public void dance(){
System.out.println("跳鬼步!");
}
在Rapper中可以增加dance方法。
3、當子類過載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入引數更寬鬆。
注意,是子類過載父類,而不是子類重寫父類。
過載的話,相當於一個全新的方法,與父類的同名方法並不衝突。兩個是同時存在的,根據傳入引數而自動選擇方法。
可以過載抽象方法,也可以過載非抽象方法。
方法的形參為什麼要比父類更寬鬆呢?
首先,形參肯定不能一致,一致的話,就是重寫了,就又回到第一條含義了。
第二,如果我們更加嚴格,那會出現什麼情況呢?
我們可以來看下面的例子。
package com.fanqiekt.principle.liskov.rapper;
import java.util.List;
/**
* 父類
*
* @author 番茄課堂-懶人
*/
public abstract class Parent {
public void setList(List<String> list){
System.out.println("執行父類setList方法");
}
}
這個是父類,setList方法有個List型別的形參。>
package com.fanqiekt.principle.liskov.rapper;
import java.util.ArrayList;
/**
* 子類
*
* @author 番茄課堂-懶人
*/
public class Children extends Parent {
public void setList(ArrayList<String> list) {
System.out.println("執行子類setList方法");
}
}
這個是子類,傳入引數型別為ArrayList,比父類更加的嚴格。
Children children = new Children();
children.setList(new ArrayList<>());
我們執行這行程式碼,看下結果。
執行子類setList方法
這個結果有沒有問題?
是有問題的,setList(new ArrayList<>())按照里氏替換原則是應該透明的執行父類的setList(List<String> list)方法的。
這塊不是很好理解,對於呼叫者來講,我想呼叫的Parent的setList(List<String> list)方法,結果卻執行Children的setList(ArrayList<String> list)方法了。
這就好像是子類重寫了父類的setList方法,而不是過載了子類的setList方法。
也就是說,方法的形參嚴格後,在某種情況就變成重寫了。
而重寫顯然是不符合里氏替換原則的。
那我們再來看看寬鬆版本的。
/**
* 子類
*
* @author 番茄課堂-懶人
*/
public class Children extends Parent {
public void setList(Collection<String> list) {
System.out.println("執行子類setList方法");
}
}
子類,傳入引數型別為Collection,比父類更加的寬鬆。
Children children = new Children();
children.setList(new ArrayList<>());
同樣的,我們執行這行程式碼,看下結果。
執行父類setList方法
Children children = new Children();
children.setList(new HashSet<>());
同樣的,我們執行這行程式碼,看下結果。
執行子類setList方法
傳入引數型別更加寬鬆,實現了子類過載父類。
4、當子類的方法實現父類的抽象方法時,方法的後置條件(即方法的返回值)要比父類更嚴格。
注意,這裡說的是重寫抽象方法,非抽象方法是不能重寫的。
為什麼說子類實現父類的抽象方法時,返回值要更嚴格呢?
package com.fanqiekt.principle.liskov.rapper;
import java.util.List;
/**
* 父類
*
* @author 番茄課堂-懶人
*/
public abstract class Parent {
public abstract List<String> getList();
}
父類,有一個getList的抽象方法,返回值為List。
package com.fanqiekt.principle.liskov.rapper;
import java.util.List;
/**
* 子類
*
* @author 番茄課堂-懶人
*/
public class Children extends Parent {
@Override
public Collection<String> getList() {
return new ArrayList<>();
}
}
子類,getList返回為Collection型別,型別更寬鬆。
會有紅線提示:… attempting to use incompatible return type 。
因為,父類返回值是List,子類返回值是List的父類Collection,透明使用父類的時候則需要將Collection轉換成List。
類向上轉換是安全的,向下轉換則不一定是安全了。
package com.fanqiekt.principle.liskov.rapper;
import java.util.List;
/**
* 子類
*
* @author 番茄課堂-懶人
*/
public class Children extends Parent {
@Override
public ArrayList<String> getList() {
return new ArrayList<>();
}
}
子類,getList返回為ArrayList型別,型別更嚴格。
將ArrayList轉換成List,向上轉換是安全的。
2、場景
八大菜系的廚師
番茄餐廳,經過兢兢業業的經營,從一家小型的餐館成長為一家大型餐廳。
廚師:老闆,我們們現在家大業大客流量也大,雖然我精力充沛,但我也架不住這麼多人的摧殘。
老闆:摧殘?你確定?
廚師:哪能,您聽錯了,是照顧,架不住這麼多人的照顧。
老闆:小火雞,可以呀,求生欲很強嘛。那你有什麼想法?
廚師:我覺得我們們可以引入八大菜系廚師,一來,什麼菜系的菜就交給什麼菜系的廚師,味道質量會更加的上乘,才能配的上我們這麼高規格的餐廳。
老闆:嗯,說的有點道理,繼續說。
廚師:二來,人手多了,還可以增加上菜的速度,三來……
老闆:有道理,馬上招聘廚師,小火雞,恭喜你,升官了,你就是未來的廚師長。因為你求生欲真的很強。
廚師長:謝謝老闆。(內心:我求生欲很強?哪裡強了?放學你別走,我讓你嚐嚐我的厲害,給你做一桌子好菜)
求生欲果真很強。
3、實現
不廢話,擼程式碼。
package com.fanqiekt.principle.liskov;
/**
* 抽象廚師類
*
* @author 番茄課堂-懶人
*/
public abstract class Chef {
/**
* 做飯
* @param dishName 餐名
*/
public void cook(String dishName){
System.out.println("開始烹飪:"+dishName);
cooking(dishName);
System.out.println(dishName + "出鍋");
}
/**
* 開始做飯
*/
protected abstract void cooking(String dishName);
}
抽象廚師類,公有cook方法,負責廚師做飯的一些相同邏輯,例如開始烹飪的準備工作,以及出鍋。
具體做飯的細節則提供一個抽象方法cooking(正在做飯),具體菜系廚師需要重寫該方法。
package com.fanqiekt.principle.liskov;
/**
* 山東廚師
*
* @author 番茄課堂-懶人
*/
public class ShanDongChef extends Chef{
@Override
protected void cooking(String dishName) {
switch (dishName){
case "蕃茄炒雞蛋":
cookingTomato();
break;
default:
throw new IllegalArgumentException("未知餐品");
}
}
/**
* 炒蕃茄雞蛋
*/
private void cookingTomato() {
System.out.println("先炒雞蛋");
System.out.println("再炒蕃茄");
System.out.println("...");
}
}
魯菜廚師ShanDongChef繼承了廚師抽象類Chef,實現了抽象方法cooking。
package com.fanqiekt.principle.liskov;
/**
* 四川廚師
*
* @author 番茄課堂-懶人
*/
public class SiChuanChef extends Chef{
@Override
protected void cooking(String dishName) {
switch (dishName){
case "酸辣土豆絲":
cookingPotato();
break;
default:
throw new IllegalArgumentException("未知餐品");
}
}
/**
* 炒酸辣土豆絲
*/
private void cookingPotato() {
System.out.println("先放蔥薑蒜");
System.out.println("再放土豆絲");
System.out.println("...");
}
}
川菜廚師SiChuanChef繼承了廚師抽象類Chef,實現了抽象方法cooking。
package com.fanqiekt.principle.liskov;
/**
* 服務員
*
* @author 番茄課堂-懶人
*/
public class Waiter {
/**
* 點餐
* @param dishName 餐名
*/
public void order(String dishName){
System.out.println("客人點餐:" + dishName);
Chef chef = new SiChuanChef();
switch(dishName) {
case "蕃茄炒雞蛋":
chef = new ShanDongChef();
break;
case "酸辣土豆絲": //取款
chef = new SiChuanChef();
break;
}
chef.cook(dishName);
System.out.println(dishName + "上桌啦,請您品嚐!");
}
}
服務員類Waiter有一個點餐order方法,根據不同的菜名去通知相應菜系的廚師去做菜。
這裡就用到了里氏替換原則,引用父類Chef可以透明地使用子類ShanDongChef或者SiChuanChef。
package com.fanqiekt.principle.liskov;
/**
* 客人
*
* @author 番茄課堂-懶人
*/
public class Client {
public static void main(String args[]){
Waiter waiter = new Waiter();
waiter.order("蕃茄炒雞蛋");
System.out.println("---------------");
waiter.order("酸辣土豆絲");
}
}
我們執行一下。
客人點餐:蕃茄炒雞蛋
開始烹飪:蕃茄炒雞蛋
先炒雞蛋
再炒蕃茄
...
蕃茄炒雞蛋出鍋
蕃茄炒雞蛋上桌啦,請您品嚐!
---------------
客人點餐:酸辣土豆絲
開始烹飪:酸辣土豆絲
先放蔥薑蒜
再放土豆絲
...
酸辣土豆絲出鍋
酸辣土豆絲上桌啦,請您品嚐!
4、優點
擼過程式碼後,我們發現替換原則的幾個優點。
里氏替換原則的核心思想就是繼承,所以優點就是繼承的優點。
程式碼重用
通過繼承父類,我們可以重用很多程式碼,例如廚師烹飪前的準備工作和出鍋。
減少建立類的成本,每個子類都擁有父類的屬性和方法。
易維護易擴充套件
通過繼承,子類可以更容易擴充套件功能。
也更容易維護了,公用方法都在父類中,特定的方法都在特定的子類中。
5、缺點
同上可知,它的缺點就是繼承的缺點。
破壞封裝
繼承是侵入性的,所以會讓子類與父類之間緊密耦合。
子類不能改變父類
可能造成子類程式碼冗餘、靈活性降低,因為子類擁有父類的所有方法和屬性。
6、嘻哈說
接下來,請您欣賞里氏替換原則的原創歌曲。
嘻哈說:里氏替換原則
作曲:懶人
作詞:懶人
Rapper:懶人
隔壁的說唱歌手可以在樂庫播放的beat freestyle歌曲
他們表演默契得體還充滿樂趣
非抽象重寫不是合理
抽象的重寫不需客氣
這是屬於他們哲理
繼承是里氏替換的核心想法
引用父類的地方透明使用子類會讓程式碼更加強大
子類可以有自己特有方法
過載父類時形參更加的廣大
不然可能覆蓋父類方法
重寫抽象方法時返回值型別要往下
因為類向上轉換可以把心放下
八大菜系每個廚師都有自己拿手的
那些共有基本功也都掌握透徹
優點是易擴充套件易維護自動繼承父類擁有的
閒來無事聽聽曲,知識已填腦中去;
學習複習新方式,頭戴耳機不小覷。
番茄課堂,學習也要酷。