里氏替換由Barbara Liskov女士提出,其給出了兩種定義:數組
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的行爲沒有發生變化,那麼類型S是類型T的子類型。)安全
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.(全部引用基類的地方必須能透明地使用其子類的對象。)ide
結合個人理解,我認爲里氏替換有兩層含義:code
不管採起繼承類或實現接口,咱們都應該遵循里氏替換原則,保證職責定義不被破壞,父類引用能安全的被子類對象替換。那麼這兩種方式,在實際開發中,有什麼須要注意的地方,應該怎麼處理嘞對象
繼承類的優勢在於可以實現便捷、直觀的共享代碼,也能實現多態,但繼承是把雙刃劍,也有須要注意的地方:繼承
基類代碼:接口
public class Base { private static final int MAX_NUM = 1000; private int[] arr = new int[MAX_NUM]; private int count; public void add(int number){ if(count<MAX_NUM){ arr[count++] = number; } } public void addAll(int[] numbers){ for(int num : numbers){ add(num); } } }
子類代碼:開發
public class Child extends Base { private long sum; @Override public void add(int number) { super.add(number); sum+=number; } @Override public void addAll(int[] numbers) { super.addAll(numbers); for(int i=0;i<numbers.length;i++){ sum+=numbers[i]; } } public long getSum() { return sum; } }
基類的add方法和addAll方法用於將數字添加到內部數組中,子類在此基礎上添加了成員變量sum,用於表示數組元素之和。get
public static void main(String[] args) { Child c = new Child(); c.addAll(new int[]{1,2,3}); System.out.println(c.getSum()); }
指望結果是1+2+3=6,但是結果倒是12。爲何嘞,這是由於子類調用的父類的addAll方法依賴的add方法同時也被子類重寫了,這裏先addALL再本身統計一遍和至關於統計了兩遍和。it
此時若想正確輸出須要咱們把子類的addAll方法修改成:
@Override public void addAll(int[] numbers) { super.addAll(numbers); }
但是,這樣又會產生新的一個問題,若是父類修改了add方法的實現爲:
public void addAll(int[] numbers){ for(int num : numbers){ if(count<MAX_NUM){ arr[count++] = num; } } }
那麼輸出又會變爲0了。
從這個例子咱們能夠看出: 若是父類內部方法可能存在依賴,重寫方法不只僅改變了被重寫的方法,同時另外一個方法(假設爲A)也致使出現了誤差,此時若按照原有的職責定義去調用父類的A方法,可能會致使出乎意料的結果。而且,若就算子類在編寫時意識到了父類方法間的依賴,修改成正確實現,那麼父類就沒法自由的修改內部實現了。
這個問題產生的緣由在於咱們重寫方法時每每容易只關注父類被重寫方法的職責定義,而容易忽視父類其餘方法是否存在依賴此方法。致使咱們仍是破壞了父類行爲的職責定義,違反了里氏替換原則,其具備必定的隱蔽性。這就要求咱們在編寫子類實現的時候必須注意到其餘方法受沒受影響。同時依賴於內部方法的父類方法也不能隨意修改,若被修改方法依賴的方法在其中一個子類被重寫。那麼就算父類在本類沒有改變職責定義,實現結果並無區別,可是若該子類調用,也有可能致使子類預期職責誤差的風險。
繼承反映的是‘是否是’的關係,假設有兩個類,鳥類有會fly()的方法,此時咱們須要添加一個企鵝類,從常識上來看企鵝應該是鳥類的子類。可是因爲企鵝的個性,他不能飛,此時就產生了矛盾,本來咱們在父類定義了鳥會飛的職責,按照里氏替換原則,咱們企鵝這個子類的fly()方法必須符合職責定義,可是實際上沒法符合,因此就沒法實現繼承,這與常識相違背。
繼承只能繼承一個類,相比接口缺乏必定靈活性。
實現接口相比繼承就靈活多了,也沒有那麼多弊端,由於接口僅僅包含職責定義,並無包含代碼實現。其優勢在於:
可是與繼承類的方式相比,也有不足的地方,其不能實現代碼的共享,雖然可以在實現類中經過注入公共類,用公共類實現代碼共享,可是卻沒有繼承便捷,直觀。