在Java中,子類別只能繼承一個父類別,繼承除了可避免類別間重複的實作定義外,還有個重要的關係,那就是子類別與父類別間會有is-a的關係,中文稱為「是一種」的關係,這是什麼意思?以先前範例來說,SwordsMan
繼承了Role
,所以SwordsMan
是一種Role
(SwordsMan
is a Role
),Magician
繼承了Role
,所以Magician
是一種Role
(Magician
is a Role
)。
為何要知道繼承時,父類別與子類別間會有「是一種」的關係?因為要開始理解多型(Polymorphism),必須先知道你操作的物件是「哪一種」東西!
來看實際的例子,以下的程式碼片段,相信你現在沒有問題地看懂,而且知道可以通過編譯:
SwordsMan swordsMan = new SwordsMan();
Magician magician = new Magician();
那你知道以下的程式片段也可以通過編譯嗎?
Role role1 = new SwordsMan();
Role role2 = new Magician();
那你知道以下的程式片段為何無法通過編譯呢?
SwordsMan swordsMan = new Role();
Magician magician = new Role();
編譯器就是語法檢查器,要知道以上程式片段為何可以通過編譯,為何無法通過編譯,就是將自己當作編譯器,檢查語法的邏輯是否正確,方式是從=
號右邊往左讀:右邊是不是一種左邊呢(右邊型態是不是左邊型態的子類別)?
從右往左讀,
SwordsMan
是不是一種Role
呢?是的!所以編譯通過。Magician
是不是一種Role
呢?是的!所以編譯通過。同樣的判斷方式,可以知道為何以下編譯失敗:SwordsMan swordsMan = new Role(); // Role是不是一種SwordsMan?
Magician magician = new Role(); // Role是不是一種Magician?
編譯器認為第一行,
Role
不一定是一種SwrodsMan
,所以編譯失敗,對於第二行,編譯器認為Role
不一定是一種Magician
,所以編譯失敗。繼續把自己當成編譯器,再來看看以下的程式片段是否可以通過編譯:Role role1 = new SwordsMan();
SwordsMan swordsMan = role1;
這個程式片段最後會編譯失敗,先從第一行看,
SwordsMan
是一種Role
,所以這行可以通過編譯。編譯器檢查這類語法,一次只看一行,就第二行而言,編譯器看到role1
為Role
宣告的名稱,於是檢查Role
是不是一種SwordsMan
,答案是不一定,所以編譯失敗在第二行!編譯器會檢查父子類別間的「是一種」關係,如果你不想要編譯器囉嗦,可以叫它住嘴:
Role role1 = new SwordsMan();
SwordsMan swordsMan = (SwordsMan) role1;
對於第二行,原本編譯器想囉嗦地告訴你,
Role
不一定是一種SwordsMan
,但你加上了(SwordsMan)
讓它住嘴了,因為這表示,你就是要讓Role
扮演(CAST)SwrodsMan
,既然你都明確要求編譯器別囉嗦了,編譯器就讓這段程式碼通過編譯了,不過後果得自行負責!以上面這個程式片段來說,
role1
確實參考至SwordsMan
實例,所以在第二行讓SwordsMan
實例扮演SwordsMan
並沒有什麼問題,所以執行時期並不會出錯。但是以下的程式片段,編譯可以成功,但執行時期會出錯:
Role role2 = new Magician();
SwordsMan swordsMan = (SwordsMan) role2;
對於第一行,
Magician
是一種Role
,可以通過編譯,對於第二行,role2
為Role
型態,編譯器原本認定Role
不一定是一種SwordsMan
而想要囉嗦,但是你明確告訴編譯器,就是要讓Role
扮演為SwordsMan
,所以編譯器就讓你通過編譯了,不過後果自負,實際上,role2
參考的是Magician
,你要讓魔法師假扮為劍士?這在執行上會是個錯誤,JVM會拋出java.lang.ClassCastException
。使用有一種(is-a)原則,你就可以判斷,何時編譯成功,何時編譯失敗,以及將扮演(CAST)看作是叫編譯器住嘴語法,並留意參考的物件實際型態,你就可以判斷何時扮演成功,何時會拋出
ClassCastException
。例如以下編譯成功,執行也沒問題:SwordsMan swordsMan = new SwordsMan();
Role role = swordsMan; // SwordsMan是一種Role
以下程式片段會編譯失敗:
SwordsMan swordsMan = new SwordsMan();
Role role = swordsMan; // SwordsMan是一種Role,這行通過編譯
SwordsMan swordsMan = role; // Role不一定是一種SwordsMan,編譯失敗
以下程式片段編譯成功,執行時也沒問題:
SwordsMan swordsMan = new SwordsMan();
Role role = swordsMan; // SwordsMan是一種Role,這行通過編譯
// 你告訴編譯器要讓Role扮演SwordsMan,以下這行通過編譯
SwordsMan swordsMan = (SwordsMan) role; // role參考SwordsMan實例,執行成功
以下程式片段編譯成功,但執行時拋出
ClassCastException
:SwordsMan swordsMan = new SwordsMan();
Role role = swordsMan; // SwordsMan是一種Role,這行通過編譯
// 你告訴編譯器要讓Role扮演Magician,以下這行通過編譯
Magician magician = (Magician) role; // role參考SwordsMan實例,執行失敗
經過以上這一連串的語法測試,好像只是在玩弄語法?不!你懂不懂以上這些東西,牽涉到寫出來的東西有沒有彈性、好不好維護的問題!
有這麼嚴重嗎?來出個題目給你吧!請設計static方法,顯示所有角色的血量!OK!上一章剛學過如何定義方法,有的人會撰寫以下的方法定義:
public static void showBlood(SwordsMan swordsMan) {
System.out.printf("%s 血量 %d%n",
swordsMan.getName(), swordsMan.getBlood());
}
public static void showBlood(Magician magician) {
System.out.printf("%s 血量 %d%n",
magician.getName(), magician.getBlood());
}
分別為
SwordsMan
與Magician
設計showBlood()
同名方法,這是重載方法的運用,如此就可以如下呼叫:showBlood(swordsMan); // swordsMan是SwordsMan型態
showBlood(magician); // magician是Magician型態
現在的問題是,目前你的遊戲中是只有
SwordsMan
與Magician
兩個角色,如果有一百個角色呢?重載出一百個方法?這種方式顯然不可能!重載的運用場合,是不同型態會有不同流程實作,如果發現重載的方法流程是類似的,設計上就有檢討的可能性。如果所有角色都是繼承自
Role
,而且你知道這些角色都是一種Role
,你就可以如下設計方法並呼叫:package cc.openhome;
public class RPG {
public static void main(String[] args) {
SwordsMan swordsMan = new SwordsMan();
swordsMan.setName("Justin");
swordsMan.setLevel(1);
swordsMan.setBlood(200);
Magician magician = new Magician();
magician.setName("Monica");
magician.setLevel(1);
magician.setBlood(100);
showBlood(swordsMan);
showBlood(magician);
}
static void showBlood(Role role) {
System.out.printf("%s 血量 %d%n",
role.getName(), role.getBlood());
}
}
在這邊僅定義了一個showBlood()
方法,參數宣告為Role
型態,第一次呼叫showBlood()
時傳入了SwordsMan
實例,這是合法的語法,因為SwordsMan
是一種Role
,第一次呼叫showBlood()
時傳入了Magician
實例也是可行,因為Magician
是一種Role
。執行的結果如下:
Monica 血量 100
這樣的寫法好處為何?就算有100種角色,只要它們都是繼承Role
,都可以使用這個方法顯示角色的血量,而不需要像先前重載的方式,為不同角色寫100個方法,多型的寫法顯然具有更高的可維護性。
什麼叫多型?以抽象講法解釋,就是使用單一介面操作多種型態的物件!若用以上的範例來理解,在showBlood()
方法中,既可以透過Role
型態操作SwordsMan
物件,也可以透過Role
型態操作Magician
物件。
稍後會學到Java中interface
的使用,在多型定義中,使用單一介面操作多種型態的物件,這邊的介面並不是專指Java中的interface
,而是指物件的公開行為,以Java具體而言,就是物件上可操作的方法。