什么是里氏代換原則
里氏代換原則(Liskov Substitution Principle LSP)面向對象設計的基本原則之一。 里氏代換原則中說,任何基類可以出現的地方,子類一定可以出現。 LSP是繼承復用的基石,只有當衍生類可以替換掉基類,軟件單位的功能不受到影響時,基類才能真正被復用,而衍生類也能夠在基類的基礎上增加新
的行為。里氏代換原則是對“開-閉”原則的補充。實現“開-閉”原則的關鍵步驟就是抽象化。而基類與子類的繼承關系就是抽象化的具體實現,所以里氏代換原則是對實現抽象化的具體步驟的規范。
簡單的理解為一個軟件實體如果使用的是一個父類,那么一定適用於其子類,而且它察覺不出父類對象和子類對象的區別。也就是說,軟件里面,把父類都替換成它的子類,程序的行為沒有變化。
但是反過來的代換卻不成立,里氏代換原則(Liskov Substitution Principle):一個軟件實體如果使用的是一個子類的話,那么它不能適用於其父類。
舉個例子解釋一下這個概念
先創建一個Person類
1 public class Person { 2 public void display() { 3 System.out.println("this is person"); 4 } 5 }
再創建一個Man類,繼承這個Person類
1 public class Man extends Person { 2 3 public void display() { 4 System.out.println("this is man"); 5 } 6 7 }
運行一下
1 public class MainClass { 2 public static void main(String[] args) { 3 Person person = new Person();//new一個Person實例 4 display(person); 5 6 Person man = new Man();//new一個Man實例 7 display(man); 8 } 9 10 public static void display(Person person) { 11 person.display(); 12 } 13 }
可以看到
運行沒有影響,符合一個軟件實體如果使用的是一個父類的話,那么一定適用於其子類,而且它察覺不出父類和子類對象的區別這句概念,這也就是java中的多態。
而反之,一個子類的話,那么它不能適用於其父類,這樣,程序就會報錯
1 public class MainClass { 2 public static void main(String[] args) { 3 Person person = new Person(); 4 display(person);//這里報錯 5 6 Man man = new Man(); 7 display(man); 8 } 9 10 public static void display(Man man) {//傳入一個子類 11 man.display(); 12 } 13 }
繼續再舉一個很經典的例子,正方形與長方形是否符合里氏代換原則,也就是說正方形是否是長方形的一個子類,
以前,我們上學都說正方形是特殊的長方形,是寬高相等的長方形,所以我們認為正方形是長方形的子類,但真的是這樣嗎?
從圖中,我們可以看到長方形有兩個屬性寬和高,而正方形則只有一個屬性邊長
所以,用代碼如此實現
1 //長方形 2 public class Changfangxing{ 3 private long width; 4 private long height; 5 6 public long getWidth() { 7 return width; 8 } 9 public void setWidth(long width) { 10 this.width = width; 11 } 12 public long getHeight() { 13 return height; 14 } 15 public void setHeight(long height) { 16 this.height = height; 17 } 18 }
1 //正方形 2 public class Zhengfangxing{ 3 private long side; 4 5 public long getSide() { 6 return side; 7 } 8 9 public void setSide(long side) { 10 this.side = side; 11 } 12 }
可以看到,它們的結構根本不同,所以正方形不是長方形的子類,所以長方形與正方形之間並不符合里氏代換原則。
當然我們也可以強行讓正方形繼承長方形
1 //正方形 2 public class Zhengfangxing extends Changfangixng{ 3 private long side; 4 5 public long getHeight() { 6 return this.getSide(); 7 } 8 9 public long getWidth() { 10 return this.getSide(); 11 } 12 13 public void setHeight(long height) { 14 this.setSide(height); 15 } 16 17 public void setWidth(long width) { 18 this.setSide(width); 19 } 20 21 public long getSide() { 22 return side; 23 } 24 25 public void setSide(long side) { 26 this.side = side; 27 } 28 }
這個樣子,編譯器是可以通過的,也可以正常使用,但是這樣就符合里氏代換原則了嗎,肯定不是的。
我們不是為了繼承而繼承,只有真正符合繼承條件的情況下我們才去繼承,所以像這樣為了繼承而繼承,強行實現繼承關系的情況也是不符合里氏代換原則的。
但這是為什么呢?,我們運行一下
1 public class MainClass { 2 public static void main(String[] args) { 3 Changfangxing changfangxing = new Changfangxing(); 4 changfangxing.setHeight(10); 5 changfangxing.setWidth(20); 6 test(changfangxing); 7 8 Changfangxing zhengfangxing = new Zhengfangxing(); 9 zhengfangxing.setHeight(10); 10 test(zhengfangxing); 11 } 12 13 public static void test(Changfangxing changfangxing) { 14 System.out.println(changfangxing.getHeight()); 15 System.out.println(changfangixng.getWidth()); 16 } 17 }
結果:
我們忽然發現,很正常啊,為什么不可以,但是我們繼續修改
1 public class MainClass { 2 public static void main(String[] args) { 3 Changfangxing changfangxing = new Changfangxing(); 4 changfangxing.setHeight(10); 5 changfangxing.setWidth(20); 6 resize(changfangxing); 7 8 Changfangxing zhengfangxing = new Zhengfangxing(); 9 zhengfangxing.setHeight(10); 10 resize(zhengfangxing); 11 } 12 13 public static void test(Changfangxing changfangxing) { 14 System.out.println(changfangxing.getHeight()); 15 System.out.println(changfangxing.getWidth()); 16 } 17 18 public static void resize(Changfangxing changfangxing) { 19 while(changfangxing.getHeight() <= changfangxing.getWidth()) { 20 changfangxing.setHeight(changfangxing.getHeight() + 1); 21 test(changfangxing); 22 } 23 } 24 }
當長方形運行時,可以正常運行,而正方形則會造成死循環,所以這種繼承方式不一定恩能夠適用於所有情況,所以不符合里氏代換原則。
還有一種形式,我們抽象出一個四邊形接口,讓長方形和正方形都實現這個接口
1 public interface Sibianxing { 2 public long getWidth(); 3 public long getHeight(); 4 }
1 public class Changfangxing implements Sibianxing{ 2 private long width; 3 private long height; 4 5 public long getWidth() { 6 return width; 7 } 8 public void setWidth(long width) { 9 this.width = width; 10 } 11 public long getHeight() { 12 return height; 13 } 14 public void setHeight(long height) { 15 this.height = height; 16 } 17 }
1 package com.ibeifeng.ex3; 2 3 public class Zhengfangxing implements Sibianxing{ 4 private long side; 5 6 public long getHeight() { 7 return this.getSide(); 8 } 9 10 public long getWidth() { 11 return this.getSide(); 12 } 13 14 public void setHeight(long height) { 15 this.setSide(height); 16 } 17 18 public void setWidth(long width) { 19 this.setSide(width); 20 } 21 22 public long getSide() { 23 return side; 24 } 25 26 public void setSide(long side) { 27 this.side = side; 28 } 29 }
運行
1 public class MainClass { 2 public static void main(String[] args) { 3 Changfangxing changfangxing = new Changfangxing(); 4 changfangxing.setHeight(10); 5 changfangxing.setWidth(20); 6 test(changfangxing); 7 8 Zhengfangxing zhengfangxing = new Zhengfangxing(); 9 zhengfangxing.setHeight(10); 10 test(zhengfangxing); 11 } 12 13 public static void test(Sibianxing sibianxing) { 14 System.out.println(sibianxing.getHeight()); 15 System.out.println(sibianxing.getWidth()); 16 } 17 }
對於長方形和正方形,取width和height是它們共同的行為,但是給width和height賦值,兩者行為不同,因此,這個抽象的四邊形的類只有取值方法,沒有賦值方法。上面的例子中那個方法只會適用於不同的子類,LSP也就不會被破壞。
注意事項
在進行設計的時候,盡量從抽象類繼承,而不是從具體類繼承。如果從繼承等級樹來看,所有葉子節點應當是具體類,而所有的樹枝節點應當是抽象類或者接口。當然這個只是一個一般性的指導原則,使用的時候還要具體情況具體分析。