里氏替換原則:LSP
定義:
如果對於每一個類型爲S的對象o1,都有類型爲T的對象o2,使得以T定義的所有程序P在所有的對象o1都換爲o2時,程序的行爲沒有發生變化,那麼S是T的子類型。
在繼承的時候,父類出現的地方子類就可以出現,子類可替代父類,因爲子類中有父類的方法,然而父類卻不可以替代子類,因爲子類中可能有父類沒有的方法。這就是所謂的向下轉型是不安全的。
使用繼承有很多優點,可以提高代碼的重用性,提高可擴展性、開放性,但是不可否認,繼承也是有缺點的:
1.繼承是侵入性的,只要繼承,就必須擁有父類的所有屬性和方法;
2.降低代碼的靈活性
3.增強了耦合性。
解決方案就是里氏替換原則。
4個含義:
1.子類必須完全實現父類的方法
2.子類可以有自己的方法
3.覆蓋或實現父類的方法時,輸入參數可以被放大
4.覆寫或實現父類的方法時,輸出結果可以被縮小
前兩個含義比較好理解,這裏就不再贅述,主要說一下3和4。
先說第3個,覆蓋或實現父類的方法時,輸入參數可以被放大。
先看一個例子:
class Father {
public Collection dosomething(HashMap map) {
System.out.println("父類被執行--->");
return map.values();
}
}
class Son extends Father {
public Collection dosomething(Map map) {
System.out.println("子類被執行--->");
return map.values();
}
}
public class Client {
public static void main(String[] args) {
// 父類存在的地方就可以替換爲子類
Father f = new Father();
HashMap map = new HashMap();
f.dosomething(map);
}
}
代碼運行結果是:
父類被執行--->
根據里氏替換原則,將父類改爲子類:
public class Client {
public static void main(String[] args) {
// 父類存在的地方就可以替換爲子類
// Father f = new Father();
Son f = new Son();
HashMap map = new HashMap();
f.dosomething(map);
}
}
然而輸出結果還是父類被執行。。。
父類方法的參數是HashMap,而子類方法的參數是Map,也就是說子類的參數類型範圍大,子類代替父類傳遞到調用者中,子類的方法永遠不會被執行。如果想要執行子類中的方法的話就需要覆寫父類中的方法,覆寫就是父類中的方法一模一樣的出現在子類中。
class Father {
public Collection dosomething(HashMap map) {
System.out.println("父類被執行--->");
return map.values();
}
}
class Son extends Father {
// public void dosomething(Map map) {
// System.out.println("子類被執行--->");
// }
@Override
public Collection dosomething(HashMap map) {
// TODO Auto-generated method stub
System.out.println("子類被執行--->");
return map.values();
}
}
public class Client {
public static void main(String[] args) {
// 父類存在的地方就可以替換爲子類
// Father f = new Father();
Son f = new Son();
HashMap map = new HashMap();
f.dosomething(map);
}
}
這是正常的。
如果父類參數的參數類型範圍大於子類輸入參數類型的話,會出現什麼問題呢?會出現父類存在的地方,子類就未必可以存在,因爲一旦把子類作爲參數傳入,調用者就很可能進入子類的方法範疇。
修改一下上面的代碼,擴大父類參數範圍,縮小子類參數範圍。
class Father {
public Collection dosomething(Map map) {
System.out.println("父類被執行--->");
return map.values();
}
}
class Son extends Father {
public Collection dosomething(HashMap map) {
System.out.println("子類被執行--->");
return map.values();
}
}
public class Client {
public static void main(String[] args) {
// 父類存在的地方就可以替換爲子類
Father f = new Father();
Son f1 = new Son();
HashMap map = new HashMap();
f.dosomething(map);
f1.dosomething(map);
}
}
f執行父類的方法,f1執行子類的方法。
這就不正常了
子類在沒有覆寫父類方法的情況下,子類方法被執行了。所以,子類中方法的參數範圍(前置條件)必須與父類的參數範圍(前置條件)相同或者更加寬鬆。
再來說一下第4個含義,覆寫或實現父類的方法時,輸出結果可以被縮小。
什麼意思呢?父類方法的返回值是類型T,子類相同方法(重載或覆寫)的返回值是S,那麼里氏替換原則就要求S必須小於等於T。也就是說,要麼S和T類型相同,要麼S是T的子類。
對於覆寫而言,父類和子類中的方法時一模一樣的,所以返回類型也應當是一樣的。
對於重載,也就是第3個含義所講到的,子類的輸入參數寬於或等於父類的參數,也就是說這個方法時不會被調用的。