讀古今文學網 > 編寫高質量代碼:改善Java程序的151個建議 > 建議148:增強類的可替換性 >

建議148:增強類的可替換性

Java的三大特徵:封裝、繼承、多態,這是每個初學者都會學習到的知識點,這裡暫且不說封裝和繼承,單單說說多態。一個接口可以有多個實現方式,一個父類可以有多個子類,並且可以把不同的實現或子類賦給不同的接口或父類。多態的好處非常多,其中有一點就是增強了類的可替換性,但是單單一個多態特性,很難保證我們的類是完全可以替換的,幸好還有一個裡氏替換原則來約束。

裡氏替換原則是說「所有引用基類的地方必須能透明地使用其子類的對象」,通俗點講,只要父類型能出現的地方子類型就可以出現,而且將父類型替換為子類型還不會產生任何錯誤或異常,使用者可能根本就不需要知道是父類型還是子類型。但是,反過來就不行了,有子類型出現的地方,父類型未必就能適應。

為了增強類的可替換性,就要求我們在設計類的時候考慮以下三點:

(1)子類型必須完全實現父類型的方法

子類型必須完全實現父類型的方法,難道還有能不實現父類型的方法?當然有,方法只是對象的行為,子類完全可以覆寫,正常情況下覆寫只會增強行為的能力,並不會「曲解」父類型的行為,一旦子類型的目的不是為了增強父類型行為,那替換的可能性就非常低了,比如這樣的代碼:


//槍

interface Gun{

//槍用來幹什麼的?射擊殺戮!

public void shoot();

}

//手槍

class Handgun implements Gun{

@Override

public void shoot(){

System.out.println("手槍射擊……");

}

}

//玩具槍

class ToyGun implements Gun{

@Override

public void shoot(){

//玩具槍不能射擊,這個方法就不實現了

}

}


上面定義了兩種類型的槍支:手槍和玩具槍,手槍可以用來射擊敵人(shoot方法),但玩具槍就完全不同了,它不能用來射擊,只是一個虛假的玩具而已,如果我們在要求使用槍支的地方傳遞了玩具槍會出現什麼問題呢?代碼如下:


public static void main(Stringargs){

Gun gun=new Handgun();

gun.shoot();

}


此處是一個手槍,用來射擊,如果使用了子類型ToyGun,士兵將會拿著玩具槍來殺人,可是射不出子彈呀!如果在CS遊戲中有這種事情發生,那就等著被人爆頭,然後看著自己淒涼的倒地。

此處不能替換的原因是子類型沒有完全實現父類型的方法,而是丟棄了父類型的行為能力,導致子類型不具備父類型的部分功能了。

(2)前置條件可以被放大

方法中的輸入參數稱為前置條件,它表達的含義是你要讓我執行,就必須滿足我的條件。前置條件是允許放大的,這樣可以保證父類型行為邏輯的繼承性,比如有這樣的代碼:


class Base{

public void doStuff(HashMap map){

}

}

class Sub extends Base{

public void doStuff(Map map){

}

}


這是Java的重載實現,子類型在實現父類型的同時也具備了自己的個性,可以處理比父類型更寬泛的任務,而且不會影響父類的任何行為,例如在如下代碼中把父類型替換為子類型就不會有任何變化:


public static void main(Stringargs){

Base b=new Base();

b.doStuff(new HashMap());

}


此時,把Base全部替換為Sub,所有的行為全部還是由父類型Base實現的,子類型的doStuff方法並沒有調用,也就是說,子類型可以在擴展前置條件的情況下保持類的可替換性。

(3)後置條件可以被縮小

父類型方法的返回值是類型T,子類同名方法(重載或覆寫)的返回值為S,那麼S可以是T的子集,這裡又分為兩種情況:

若是覆寫,父類型和子類型的方法名名稱就會相同,輸入參數也相同(前置條件相同),只是返回值S是T類型的子集,子類型替換父類型完全沒有問題。

若是重載,方法的輸入參數類型或數量則不相同(前置條件不同),在使用子類型替換父類型的情況下,子類型的方法不會被調用到的,已經無關返回值類型了,此時子類依然具備可替換性。

增強類的可替換性,則增強了程序的健壯性,版本升級時也可以保持非常好的兼容性。即使增加子類,原有的子類還可以繼續運行。在實際項目中,每個子類對應不同的業務含義,使用父類作為參數,傳遞不同的子類完成不同的業務邏輯,非常完美!