**里氏替換原則**
面向對象設計原則的一種,也叫里氏代換原則。里氏替換原則是關于繼承的一個原則,遵循里氏替換原則能夠更好地發揮繼承的作用。
**里氏替換原則的官方定義**
* 如果對每一個類型為 T1的對象 o1,都有類型為 T2 的對象o2,使得以 T1定義的所有程序 P 在所有的對象 o1 都代換成 o2 時,程序 P 的行為沒有發生變化,那么類型 T2 是類型 T1 的子類型
* 所有引用基類的地方必須能透明地使用其子類的對象
**里氏替換原則解讀**
里氏替換原則強調的是設計和實現要依賴于抽象而非具體;子類只能去擴展基類,而不是隱藏或者覆蓋基類,它包含以下4層含義:
**1)子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法**
子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法,父類中凡是已經實現好的方法(相對于抽象方法而言),實際上是在設定一系列的規范和契約,雖然它不強制要求所有的子類必須遵從這些契約,但是如果子類對這些非抽象方法任意修改,就會對整個繼承體系造成破壞
舉例:
```
public class C {
public int func(int a, int b){
return a+b;
}
}
public class C1 extends C{
@Override
public int func(int a, int b) {
return a-b;
}
}
public class Client{
public static void main(String[] args) {
C c = new C1();
System.out.println("2+1=" + c.func(2, 1));
}
}
```
運行結果:2+1=1
上面的運行結果明顯是錯誤的。類C1繼承C,后來需要增加新功能,類C1并沒有新寫一個方法,而是直接重寫了父類C的func方法,違背里氏替換原則,引用父類的地方并不能透明的使用子類的對象,導致運行結果出錯。
**2)子類可以有自己的個性**
在繼承父類屬性和方法的同時,每個子類也都可以有自己的個性,在父類的基礎上擴展自己的功能。前面其實已經提到,當功能擴展時,子類盡量不要重寫父類的方法,而是另寫一個方法,所以對上面的代碼加以更改,使其符合里氏替換原則,代碼如下:
```
public class C {
public int func(int a, int b){
return a+b;
}
}
public class C1 extends C{
public int func2(int a, int b) {
return a-b;
}
}
public class Client{
public static void main(String[] args) {
C1 c = new C1();
System.out.println("2-1=" + c.func2(2, 1));
}
}
```
運行結果:2-1=1
**3)覆蓋或實現父類的方法時輸入參數可以被放大**
當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬松
```
public class ParentClazz {
public void say(CharSequence str) {
System.out.println("parent execute say " + str);
}
}
public class ChildClazz extends ParentClazz {
public void say(String str) {
System.out.println("child execute say " + str);
}
}
/**
* 測試
*/
public class Main {
public static void main(String[] args) {
ArrayList list = new ArrayList();
ParentClazz parent = new ParentClazz();
parent.say("hello");
ChildClazz child = new ChildClazz();
child.say("hello");
}
}
執行結果:
parent execute say hello
child execute say hello
```
以上代碼中我們并沒有重寫父類的方法,只是重載了同名方法,具體的區別是:子類的參數 String 實現了父類的參數 CharSequence。此時執行了子類方法,在實際開發中,通常這不是我們希望的,父類一般是抽象類,子類才是具體的實現類,如果在方法調用時傳遞一個實現的子類可能就會產生非預期的結果,引起邏輯錯誤,根據里氏替換的子類的輸入參數要寬于或者等于父類的輸入參數,我們可以修改父類參數為String,子類采用更寬松的 CharSequence,如果你想讓子類的方法運行,就必須覆寫父類的方法。代碼如下:
```
public class ParentClazz {
public void say(String str) {
System.out.println("parent execute say " + str);
}
}
public class ChildClazz extends ParentClazz {
public void say(CharSequence str) {
System.out.println("child execute say " + str);
}
}
public class Main {
public static void main(String[] args) {
ParentClazz parent = new ParentClazz();
parent.say("hello");
ChildClazz child = new ChildClazz();
child.say("hello");
}
}
執行結果:
parent execute say hello
parent execute say hello
```
**4.覆寫或實現父類的方法時輸出結果可以被縮小**
當子類的方法實現父類的抽象方法時,方法的后置條件(即方法的返回值)要比父類更嚴格
```
public abstract class Father {
public abstract Map hello();
}
public class Son extends Father {
@Override
public Map hello() {
HashMap map = new HashMap();
System.out.println("son execute");
return map;
}
}
public class Main {
public static void main(String[] args) {
Father father = new Son();
father.hello();
}
}
執行結果:
son execute
```
*****
**里氏替換原則優點**
保證了父類的復用性,同時也能夠降低系統出錯誤的故障,防止誤操作,同時也不會破壞繼承的機制,這樣繼承才顯得更有意義。
增強程序的健壯性,版本升級是也可以保持非常好的兼容性.即使增加子類,原有的子類還可以繼續運行.在實際項目中,每個子類對應不同的業務含義,使用父類作為參數,傳遞不同的子類完成不同的業務邏輯,完美!
*****
**里氏替換原則總結**
繼承作為面向對象三大特性之一,在給程序設計帶來巨大便利的同時,也帶來了弊端。比如使用繼承會給程序帶來侵入性,程序的可移植性降低,增加了對象間的耦合性,如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,并且父類修改后,所有涉及到子類的功能都有可能會產生故障。
里氏替換原則的目的就是增強程序健壯性,版本升級時也可以保持非常好的兼容性。
有人會說我們在日常工作中,會發現在自己編程中常常會違反里氏替換原則,程序照樣跑的好好的。所以大家都會產生這樣的疑問,假如我非要不遵循里氏替換原則會有什么后果?后果就是:你寫的代碼出問題的幾率將會大大增加。