在學習java類的繼承時,我們知道繼承有一些優(yōu)點:
- 子類擁有父類的所有方法和屬性,從而可以減少創(chuàng)建類的工作量。
- 提高了代碼的重用性。
- 提高了代碼的擴展性,子類不但擁有了父類的所有功能,還可以添加自己的功能。
但又有點也同樣存在缺點:
- 繼承是侵入性的。只要繼承,就必須擁有父類的所有屬性和方法。
- 降低了代碼的靈活性。因為繼承時,父類會對子類有一種約束。
- 增強了耦合性。當需要對父類的代碼進行修改時,必須考慮到對子類產(chǎn)生的影響。有時修改了一點點代碼都有可能需要對打斷程序進行重構。
如何揚長避短呢?方法是引入里氏替換原則。
定義
第一種定義,也是最正宗的定義:If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
如果對每一個類型為S的對象o1,都有類型為T的對象o2,使得以T定義的所有程序P在所有的對象o1都代換成o2時,程序P的行為沒有發(fā)生變化,那么類型S是類型T的子類型。第二種定義:Functions that use pointers or references to base classes must be able to useobjects of derived classes without knowing it.
所有引用基類的地方必須能透明地使用其子類的對象。
第二種定義比較通俗,容易理解:只要有父類出現(xiàn)的地方,都可以用子類來替代,而且不會出現(xiàn)任何錯誤和異常。但是反過來則不行,有子類出現(xiàn)的地方,不能用其父類替代。
四層含義
里氏替換原則對繼承進行了規(guī)則上的約束,這種約束主要體現(xiàn)在四個方面:
- 子類必須實現(xiàn)父類的抽象方法,但不得重寫(覆蓋)父類的非抽象(已實現(xiàn))方法。
- 子類中可以增加自己特有的方法。
- 當子類覆蓋或實現(xiàn)父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入?yún)?shù)更寬松。
- 當子類的方法實現(xiàn)父類的抽象方法時,方法的后置條件(即方法的返回值)要比父類更嚴格。
下面對以上四個含義進行詳細的解釋:
子類必須實現(xiàn)父類的抽象方法,但不得重寫(覆蓋)父類的非抽象(已實現(xiàn))方法
在我們做系統(tǒng)設計時,經(jīng)常會設計接口或抽象類,然后由子類來實現(xiàn)抽象方法,這里使用的其實就是里氏替換原則。若子類不完全對父類的方法進行實例化,那么子類就不能被實例化,那么這個接口或抽象類就毫無存在的意義了。
里氏替換原則規(guī)定,子類不能覆寫父類已實現(xiàn)的方法。父類中已實現(xiàn)的方法其實是一種已定好的規(guī)范和契約,如果我們隨意的修改了它,那么可能會帶來意想不到的錯誤。
下面舉例說明一下子類覆寫了父類方法帶來的后果。
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
我們想要的結果是“1+2=3”??梢钥吹剑椒ㄖ貙懞蠼Y果就不是了我們想要的結果了,也就是這個程序中子類B不能替代父類A。這違反了里氏替換原則原則,從而給程序造成了錯誤。
有時候父類有多個子類,但在這些子類中有一個特例。要想滿足里氏替換原則,又想滿足這個子類的功能時,有的伙伴可能會修改父類的方法。但是,修改了父類的方法又會對其他的子類造成影響,產(chǎn)生更多的錯誤。這是怎么辦呢?我們可以為這個特例創(chuàng)建一個新的父類,這個新的父類擁有原父類的部分功能,又有不同的功能。這樣既滿足了里氏替換原則,又滿足了這個特例的需求。
子類中可以增加自己特有的方法
這個很容易理解,子類繼承了父類,擁有了父類和方法,同時還可以定義自己有,而父類沒有的方法。這是在繼承父類方法的基礎上進行功能的擴展,符合里氏替換原則。
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
這是子類的新方法...
當子類覆蓋或實現(xiàn)父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入?yún)?shù)更寬松
先看一段代碼:
import java.util.HashMap;
public class A {
public void fun(HashMap map){
System.out.println("父類被執(zhí)行...");
}
}
import java.util.Map;
public class B extends A{
public void fun(Map map){
System.out.println("子類被執(zhí)行...");
}
}
import java.util.HashMap;
public class demo {
public 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);
}
}
運行結果:
父類的運行結果:父類被執(zhí)行...
子類替代父類后的運行結果:父類被執(zhí)行...
我們應當主意,子類并非重寫了父類的方法,而是重載了父類的方法。因為子類和父類的方法的輸入?yún)?shù)是不同的。子類方法的參數(shù)Map比父類方法的參數(shù)HashMap的范圍要大,所以當參數(shù)輸入為HashMap類型時,只會執(zhí)行父類的方法,不會執(zhí)行子類的重載方法。這符合里氏替換原則。
但如果我將子類方法的參數(shù)范圍縮小會怎樣?看代碼:
import java.util.Map;
public class A {
public void fun(Map map){
System.out.println("父類被執(zhí)行...");
}
}
import java.util.HashMap;
public class B extends A{
public void fun(HashMap map){
System.out.println("子類被執(zhí)行...");
}
}
import java.util.HashMap;
public class demo {
public static void main(String[] args){
System.out.print("父類的運行結果:");
A a=new A();
HashMap map=new HashMap();
a.fun(map);
//父類存在的地方,可以用子類替代
//子類B替代父類A
System.out.print("子類替代父類后的運行結果3");
B b=new B();
b.fun(map);
}
}
運行結果:
父類的運行結果:父類被執(zhí)行...
子類替代父類后的運行結果:子類被執(zhí)行...
呵呵!在父類方法沒有被重寫的情況下,子方法被執(zhí)行了,這樣就引起了程序邏輯的混亂。所以子類中方法的前置條件必須與父類中被覆寫的方法的前置條件相同或者更寬松。
當子類的方法實現(xiàn)父類的(抽象)方法時,方法的后置條件(即方法的返回值)要比父類更嚴格
示例代碼:
import java.util.Map;
public abstract class A {
public abstract Map fun();
}
import java.util.HashMap;
public class B extends A{
@Override
public HashMap fun(){
HashMap b=new HashMap();
b.put("b","子類被執(zhí)行...");
return b;
}
}
import java.util.HashMap;
public class demo {
public static void main(String[] args){
A a=new B();
System.out.println(a.fun());
}
}
運行結果:
{b=子類被執(zhí)行...}
若在繼承時,子類的方法返回值類型范圍比父類的方法返回值類型范圍大,在子類重寫該方法時編譯器會報錯。
總結
java采用的單繼承相較于c++的多繼承,總體上來看是“利”多于“弊”的。采用里氏替換原則可以讓“利”的因素發(fā)揮最大的作用,并減少“弊”帶來的諸多麻煩。