里氏替換原則的定義是:所有引用基類的地方必須能透明化地使用其子類的對象。
里氏替換原則針對的問題
有一個功能P1,由類A完成。現需要將功能P1進行擴展,擴展后的功能為P,其中P由原有功能P1與新功能P2組成。新功能P由類A的子類B來完成,則子類B在完成新功能P2的同時,有可能會導致原有功能P1發生故障。
里氏替換原則的解決方案
當使用繼承的時候,遵循里氏替換原則。類B繼承類A的時候,除了添加新的方法完成新增功能P2外,盡量不要重寫父類A的方法,也盡量不要重載父類A的方法。
繼承包含這樣一層含義:父類中范式已經實現好的方法(相對於抽象方法而言),實際上是在設定一系列的規范和契約,雖然它不強制要求所有的子類必須遵從這些契約,但是如果子類對這些非抽象方法任意修改,就會對整個繼承體系造成破壞。而里氏替換原則就是表達了這一層含義。
繼承作為面向對象的三大特征之一,在給程序設計帶來巨大便利的同時,也帶來了弊端。比如使用繼承會給程序帶來侵入性,程序的可移植性降低,增加了對象之間的耦合性。如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,並且父類修改后,所有涉及到子類的功能都有可能會產生故障。
里氏替換原則的案例
關於里氏替換原則的例子,最有名的是【正方形不是長方形】。當然,生活中也有很多類似的例子,例如,企鵝、鴕鳥和幾維鳥從生物學的角度來划分,它們屬於鳥類;但是從類的繼承關系來看,由於它們不能繼承鳥類會飛的功能,所以它們不能被定義成鳥類的子類。如果要強行定義為鳥類的子類,則有一些行為是需要重寫鳥類的行為的,這樣就違背了里氏替換原則。
這里以【幾維鳥不是鳥】為例來說明里氏替換原則
代碼如下:
public class LspTest { public static void main(String[] args) { Bird bird1 = new Swallow(); Bird bird2 = new BrownKiwi(); bird1.setSpeed(120); bird2.setSpeed(120); System.out.println("如果飛行300公里:"); try { System.out.println("燕子將飛行" + bird1.getFlyTime(300) + "小時。"); // 燕子將飛行2.5小時。 System.out.println("幾維鳥將飛行" + bird2.getFlyTime(300) + "小時。"); // 幾維鳥將飛行Infinity小時。 } catch (Exception err) { System.out.println("發生錯誤了!"); } } } // 鳥類 class Bird { double flySpeed; public void setSpeed(double speed) { flySpeed = speed; } public double getFlyTime(double distance) { return (distance / flySpeed); } } // 燕子類 class Swallow extends Bird { } // 幾維鳥類 class BrownKiwi extends Bird { public void setSpeed(double speed) { flySpeed = 0; } }
這里,因為幾維鳥類重寫了鳥類的setSpeed方法,導致了程序發生錯誤,違背了里氏替換原則。正確的做法應該是取消幾維鳥原來的繼承關系,定義鳥類和幾位鳥類的更一般的父類,比如動物類。他們都擁有奔跑的能力。幾維鳥的飛行速度雖然為0,但是奔跑速度不為0,可以計算出其奔跑300千米所要花費的時間。
修改后的代碼如下:
public class LspTest2 { public static void main(String[] args) { Animal bird_swallow = new Bird_Swallow(); Animal notBird_brownKiwi = new NotBird_BrownKiwi(); bird_swallow.setSpeed(120); notBird_brownKiwi.setSpeed(120); System.out.println("如果移動300公里:"); try { System.out.println("燕子將花費" + bird_swallow.getFlyTime(300) + "小時。"); // 燕子將花費2.5小時。 System.out.println("幾維鳥將花費" + notBird_brownKiwi.getFlyTime(300) + "小時。"); // 幾維鳥將花費2.5小時。 } catch (Exception err) { System.out.println("發生錯誤了!"); } } } // 動物類 class Animal { private double speed; public void setSpeed(double speed) { this.speed = speed; } public double getFlyTime(double distance) { return (distance / speed); } } // 燕子類 class Bird_Swallow extends Animal { } // 幾維鳥類 class NotBird_BrownKiwi extends Animal { }
里氏替換原則的認識
里氏替換原則主要闡述了有關繼承的一些原則,也就是什么時候應該使用繼承,什么時候不應該使用繼承,以及其中蘊含的原理。里氏替換原則是繼承復用的基礎,它反映了基類與子類之間的關系,是對開閉原則的補充,是對實現抽象化的具體步驟的規范。
里氏替換原則的通俗理解就是:子類可以擴展父類的功能,但是不能改變父類原有的功能。也就是說,在子類繼承父類的時候,除了添加新的方法完成新增功能之外,盡量不要重寫父類的方法。
如果通過重寫父類的方法來完成新的功能,這樣寫起來雖然簡單,但是整個繼承體系的可復用性會比較差,特別是運用多態比較頻繁時,程序運行出錯的概率會非常大。
如果程序違背了里氏替換原則,則繼承的對象在基類出現的地方會出現運行錯誤。這時其修正方法是:取消原來的繼承關系,重新設計他們之間的關系。
它包含了以下4層含義:
1.子類可以實現父類的抽象方法,但是不能覆蓋父類的非抽象方法。
2.子類中可以增加自己特有的方法。
3.當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更加寬松。
"每天按部就班地做着自己的事,但是也會對你淡淡地想念。打開微信偶爾平常得聊幾句,問問最近你過得怎么樣,開心不開心,和你分享一些今天遇到的趣事,也不期待你能夠秒回。只是因為分享的對象是你,我就會覺得很滿足了。"