里氏替換原則(Liskov Substitution Principle,簡稱LSP): 子類可以替換父類
繼承有一些優點:
1. 提高代碼的重用性,子類擁有父類的方法和屬性;
2. 提高代碼的可擴展性,子類可形似於父類,但異於父類,保留自我的特性;
缺點:侵入性、不夠靈活、高耦合
1. 繼承是侵入性的,只要繼承就必須擁有父類的所有方法和屬性,在一定程度上約束了子類,降低了代碼的靈活性;
2. 增加了耦合,當父類的常量、變量或者方法被修改了,需要考慮子類的修改,所以一旦父類有了變動,很可能會造成
非常糟糕的結果,要重構大量的代碼。
任何基類可以出現的地方,子類一定可以出現。里氏替換原則是繼承復用的基石,只有當衍生類可以替換基類,軟件單位的功能不受到影響時,即基類隨便怎么改動子類都不受此影響,那么基類才能真正被復用
因為繼承帶來的侵入性,增加了耦合性,也降低了代碼靈活性,父類修改代碼,子類也會受到影響,此時就需要里氏替換原則。
- 子類必須實現父類的抽象方法,但不得重寫(覆蓋)父類的非抽象(已實現)方法。
- 子類中可以增加自己特有的方法。
- 當子類覆蓋或實現父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬松。
- 當子類的方法實現父類的抽象方法時,方法的后置條件(即方法的返回值)要比父類更嚴格。
a.子類必須實現父類的抽象方法,但不得重寫(覆蓋)父類的非抽象(已實現)方法。
public class A {
public void fun(int a,int b){
System.out.println(a+"+"+b+"="+(a+b));
}
}
public class B extends A{
@Override
public void fun(int a,int b){
System.out.println(a+"-"+b+"="+(a-b));
}
}
public class demo {
public static void main(String[] args){
System.out.println("父類的運行結果");
A a=new A();
a.fun(1,2);
//父類存在的地方,可以用子類替代
//子類B替代父類A
System.out.println("子類替代父類后的運行結果");
B b=new B();
b.fun(1,2);
}
}
運行結果:
父類的運行結果
1+2=3
子類替代父類后的運行結果
1-2=-1
b.子類中可以增加自己特有的方法。
public class A {
public void fun(int a,int b){
System.out.println(a+"+"+b+"="+(a+b));
}
}
public class B extends A{
public void newFun(){
System.out.println("這是子類的新方法...");
}
}
public class demo {
public static void main(String[] args){
System.out.print("父類的運行結果:");
A a=new A();
a.fun(1,2);
//父類存在的地方,可以用子類替代
//子類B替代父類A
System.out.print("子類替代父類后的運行結果:");
B b=new B();
b.fun(1,2);
//子類B的新方法
b.newFun();
}
}
運行結果:
父類的運行結果:1+2=3
子類替代父類后的運行結果:1+2=3
這是子類的新方法...
c.當子類覆蓋或實現父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬松。
public class LSP {
class A {
public void fun(HashMap map){
System.out.println("父類被執行...");
}
}
class B extends A{
public void fun(Map map){
System.out.println("子類被執行...");
}
}
public static void main(String[] args){
System.out.print("父類的運行結果:");
LSP lsp =new LSP();
LSP.A a= lsp.new A();
HashMap<Object, Object> map=new HashMap<Object, Object>();
a.fun(map);
//父類存在的地方,可以用子類替代
//子類B替代父類A
System.out.print("子類替代父類后的運行結果:");
LSP.B b=lsp.new B();
b.fun(map);
}
}
運行結果:
父類的運行結果:父類被執行...
子類替代父類后的運行結果:父類被執行...
符合條件
我們應當注意,子類並非重寫了父類的方法,而是重載了父類的方法。因為子類和父類的方法的輸入參數是不同的。
子類方法的參數Map比父類方法的參數HashMap的范圍要大,所以當參數輸入為HashMap類型時,只會執行父類的方法,不會執行父類的重載方法。這符合里氏替換原則。
//將子類方法的參數范圍縮小會怎樣?
import java.util.Map;
public class A {
public void fun(Map map){
System.out.println("父類被執行...");
}
}
import java.util.HashMap;
public class B extends A{
public void fun(HashMap map){
System.out.println("子類被執行...");
}
}
import java.util.HashMap;
public class demo {
static void main(String[] args){
System.out.print("父類的運行結果:");
A a=new A();
HashMap map=new HashMap();
a.fun(map);
//父類存在的地方,都可以用子類替代
//子類B替代父類A
System.out.print("子類替代父類后的運行結果:");
B b=new B();
b.fun(map);
}
}
運行結果: 父類的運行結果:父類被執行... 子類替代父類后的運行結果:子類被執行... 在父類方法沒有被重寫的情況下,子方法被執行了,這樣就引起了程序邏輯的混亂。
所以子類中方法的前置條件必須與父類中被覆寫的方法的前置條件相同或者更寬松。不符合里式替換
d.當子類的方法實現父類的抽象方法時,方法的后置條件(即方法的返回值)要比父類更嚴格。
public class LSP1 {
abstract class A {
public abstract Map fun();
}
class B extends A{
@Override
public HashMap fun(){
HashMap b=new HashMap();
b.put("b","子類被執行...");
return b;
}
}
public static void main(String[] args){
LSP1 lsp =new LSP1();
LSP1.A a=lsp.new B();
System.out.println(a.fun());
}
}
運行結果:
{b=子類被執行...}
若在繼承時,子類的方法返回值類型范圍比父類的方法返回值類型范圍大,在子類重寫該方法時編譯器會報錯。

看上去很不可思議,因為我們會發現在自己編程中常常會違反里氏替換原則,程序照樣跑的好好的。所以大家都會產生這樣的疑問,假如我非要不遵循里氏替換原則會有什么后果?
后果就是:你寫的代碼出問題的幾率將會大大增加。

