行為的多型


多型與 is-a 關係 曾試著當編譯器,判斷哪些繼承多型語法可以通過編譯,加入扮演(Cast)語法的目的又是為何,以及哪些情況下,執行時期會扮演失敗,哪些又可扮演成功。會使用介面定義行為之後,也要再來當編譯器,看看哪些是合法的多型語法。例如:

Swimmer swimmer1 = new Shark();
Swimmer swimmer2 = new Human();
Swimmer swimmer3 = new Submarine();

這三行程式碼都可以通過編譯,判斷方式是「右邊是不是擁有左邊的行為」,或「是右邊物件是不是實作了左邊介面」。

是否擁有行為?是否實作介面?


Shark擁有Swimmer行為嗎?有的!因為Fish實作了Swimmer介面,也就是Fish擁有Swimmer行為,Shark繼承Fish,當然也擁有Swimmer行為,所以通過編譯,HumanSubmarine也都實作了Swimmer介面,所以通過編譯。

更進一步地,來看看底下的程式碼是否可通過編譯?

Swimmer swimmer = new Shark();
Shark shark = swimmer;

第一行要判斷Shark是否擁有Swimmer行為?是的!可通過編譯,但第二行呢?swimmerSwimmer型態,編譯器看到該行會想到,有Swimmer行為的物件是不是Shark呢?這可不一定!也許實際上是Human實例!因為有Swimmer行為的物件不一定是Shark,所以第二行編譯失敗!

就上面的程式碼片段而言,實際上swimmer是參考至Shark實例,你可以加上扮演(Cast)語法:

Swimmer swimmer = new Shark();
Shark shark = (Shark) swimmer;

對第二行的語意而言,就是在告訴編譯器,對!你知道有Swimmer行為的物件,不一定是Shark,不過你就是要它扮演Shark,所以編譯器就別再囉嗦了。可以通過編譯,執行時期swimmer確實也是參考Shark實例,所以也沒有錯誤。

底下的程式片段會在第二行編譯失敗:

Swimmer swimmer = new Shark();
Fish fish = swimmer;

第二行swimmerSwimmer型態,所以編譯器會問,實作Swimmer介面的物件是不是繼承Fish?不一定,也許是Submarine!因為會實作Swimmer介面的並不一定繼承Fish,所以編譯失敗了。如果加上扮演語法:

Swimmer swimmer = new Shark();
Fish fish = (Fish) swimmer;

第二行告訴編譯器,你知道有Swimmer行為的物件,不一定繼承Fish,不過你就是要它扮演Fish,所以編譯器就別再囉嗦了。可以通過編譯,執行時期swimmer確實也是參考Shark實例,它是一種Fish,所以也沒有錯誤。

下面這個例子就會拋出ClassCastException錯誤:

Swimmer swimmer = new Human();
Shark shark = (Shark) swimmer;

在第二行,swimmer實際上參考了Human實例,你要他扮演鯊魚?這太荒繆了!所以執行時就出錯了。類似地,底下的例子也會出錯:

Swimmer swimmer = new Submarine();
Fish fish = (Fish) swimmer;

在第二行,swimmer實際上參考了Submarine實例,你要他扮演魚?又不是在演哆啦A夢中海底鬼岩城,哪來的機器魚情節!Submarine不是一種Fish,執行時期會因為扮演失敗而拋出ClassCastException

知道以下的語法,哪些可以通過編譯,哪些可以扮演成功作什麼?來考慮一個需求,寫個staticswim()方法,讓會游的東西都游起來,在不會使用介面多型語法時,也許你會寫下:

public static void doSwim(Fish fish) {
    fish.swim();
}
public static void doSwim(Human human) {
    human.swim();
}
public static void doSwim(Submarine submarine) {
    submarine.swim();
}

老實說,如果已經會寫下接收FishdoSwim()版本,程式觀念還算是不錯的,因為至少你知道,只要有繼承Fish,無論是AnemonefishSharkPiranha,都可以使用FishdoSwim()版本,至少你會使用繼承時的多型,至於HumanSubmarine各使用其接收HumanSubmarinedoSwim()版本。

問題是,如果「種類」很多怎麼辦?多了水母、海蛇、蟲等種類呢?每個種類重載一個方法出來嗎?其實在你的設計中,會游泳的東西,都擁有Swimmer的行為,都實作了Swimmer介面,所以你只要這麼設計就可以了:

package cc.openhome;

public class Ocean {
    public static void main(String[] args) {
        doSwim(new Anemonefish("尼莫"));
        doSwim(new Shark("蘭尼"));
        doSwim(new Human("賈斯汀"));
        doSwim(new Submarine("黃色一號"));
    }

    static void doSwim(Swimmer swimmer) {
        swimmer.swim();
    }
}

執行結果如下:

小丑魚 尼莫 游泳
鯊魚 蘭尼 游泳
人類 賈斯汀 游泳
潛水艇 黃色一號 潛行

只要是實作Swimmer介面的物件,都可以使用範例中的doSwim()方法,AnemonefishSharkHumanSubmarine等,都實作了Swimmer介面,再多種類,只要物件擁有Swimmer行為,你就都不用撰寫新的方法,可維護性顯然提高許多!