LSP全稱
LSP , Liskov Substitution Principle , 里氏替換原則
定義
所有引用基類(父類)的地方必須能透明地使用其子類的對象。通俗講:子類可以擴展父類的功能,但不能改變父類原有的功能
優點
- 是實現開閉原則的重要方式之一
- 克服了繼承中重寫父類造成的可用性變差的缺點
- 它是動作正確性的保證。即類的擴展不會給已有的系統引入新的錯誤,降低了代碼出錯的可能性
缺點
- 繼承是侵入性的,只要繼承就必須擁有父類的所有屬性和方法
- 可能造成子類代碼冗余,靈活性降低,因為必須擁有父類的屬性和方法
實現
- 問題由來: 有一功能P1,由類A完成。現需要將功能P1進行擴展,擴展后的功能為P,其中P由原有功能P1與新功能P2組成。新功能P由類A的子類B來完成,則子類B在完成新功能P2的同時,有可能會導致原有功能P1發生故障。
- 解決方案:
當使用繼承時,遵循里氏替換原則。類B繼承類A時,除添加新的方法完成新增功能P2外,盡量不要重寫父類A的方法,也盡量不要重載父類A的方法。
里氏替換原則的核心原理是抽象,抽象又依賴於繼承這個特性
- 子類必須完全實現父類的方法
- 子類可以有自己的修改(重寫,重載,添加父類中沒有的方法)
- 子類中override的方法,傳入參數類型必須是與父類相同類型,或是子類型
- 子類中override的方法,返回類型必須是與父類相同類型,或是子類型
實例
舉例說明兩個數相減,由A類負責
class A {
public int func1(int a, int b) {
return a-b;
}
}
public class LSPClient {
public static void main(String[] args) {
A a = new A();
System.out.println("5-3=" + a.func1(5, 3));
System.out.println("100-10=" + a.func1(100, 10));
}
}
輸出結果:
5 - 3 = 2
100 - 10 = 90
此時需要新加一個功能:兩數相加,然后再與100求和,由B類負責
class A {
public int func1(int a, int b) {
return a - b;
}
}
class B extends A {
@Override
public int func1(int a, int b) {
return a + b;
}
public int func2(int a, int b) {
return func1(a, b) + 100;
}
}
public class LSPClient {
public static void main(String[] args) {
B b = new B();
System.out.println("5-3=" + b.func1(5, 3));
System.out.println("100-10=" + b.func1(100, 10));
System.out.println("50+20+100=" + b.func2(50, 20));
}
}
輸出結果:
5-3=8
100-10=110
50+20+100=170
此時發現原本運行正常的相減功能發生了錯誤,原因是子類B重寫了父類A中的func1方法,從兩數相減變成了兩數相加。引用基類A完成的功能,換成子類B之后,發生了異常。在實際編程中,我們常常會通過重寫父類的方法來完成新的功能,這樣寫起來雖然簡單,但是整個繼承體系的可復用性會比較差,特別是運用多態比較頻繁時,程序運行出錯的幾率非常大。如果非要重寫父類的方法,比較通用的做法是:原來的父類和子類都繼承一個更通俗的基類,原有的繼承關系去掉,采用依賴、聚合,組合等關系代替。
里氏替換原則通俗的來講就是:子類可以擴展父類的功能,但不能改變父類原有的功能。包含以下4層含義:
- 子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
- 子類中可以增加自己特有的方法。
- 當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬松。
當子類的方法實現父類的抽象方法時,方法的后置條件(即方法的返回值)要比父類更嚴格。 - 看上去很不可思議,因為我們會發現在自己編程中常常會違反里氏替換原則,程序照樣跑的好好的。所以大家都會產生這樣的疑問,假如我非要不遵循里氏替換原則會有什么后果?后果就是:你寫的代碼出問題的幾率將會大大增加。