java se 7 技術手冊第六章草稿 - 何謂繼承?

15
Java SE 7 6.1 何謂 何謂 何謂 何謂繼承 繼承 繼承 繼承? 物件導向中,子類別繼承(Inherit)父類別,避免重複的行為定義,不過並非為 了避免重複定義行為就使用繼承,濫用繼承而導致程式維護上的問題時有所聞,如何 正確判斷使用繼承的時機,以及繼承之後如何活用多型,才是學習繼承時的重點。 6.1.1 繼承 繼承 繼承 繼承共同行為 共同行為 共同行為 共同行為 繼承基本上就是避免多個類別間重複定義共同行為。以實際的例子來說明比較清 楚,假設你在正開發一款 RPGRole-playing game)遊戲,一開始設定的角色有劍 士與魔法師。首先你定義了劍士類別: public class SwordsMan { private String name; // private int level; // private int blood; // public void fight() { System.out.println(" "); } public int getBlood() { return blood; } public void setBlood(int blood) { this.blood = blood; } public int getLevel() { return level; } public void setLevel(int level) { this.level = level; } public String getName() { return name; } public void setName(String name) {

Upload: justin-lin

Post on 13-Jul-2015

2.155 views

Category:

Technology


1 download

TRANSCRIPT

Java SE 7技術手冊草稿

6.1 何謂何謂何謂何謂繼承繼承繼承繼承???? 物件導向中,子類別繼承(Inherit)父類別,避免重複的行為定義,不過並非為

了避免重複定義行為就使用繼承,濫用繼承而導致程式維護上的問題時有所聞,如何

正確判斷使用繼承的時機,以及繼承之後如何活用多型,才是學習繼承時的重點。

6.1.1 繼承繼承繼承繼承共同行為共同行為共同行為共同行為

繼承基本上就是避免多個類別間重複定義共同行為。以實際的例子來說明比較清

楚,假設你在正開發一款 RPG(Role-playing game)遊戲,一開始設定的角色有劍

士與魔法師。首先你定義了劍士類別:

public class SwordsMan {

private String name; // 角色名稱

private int level; // 角色等級

private int blood; // 角色血量

public void fight() {

System.out.println("揮劍攻擊");

}

public int getBlood() {

return blood;

}

public void setBlood(int blood) {

this.blood = blood;

}

public int getLevel() {

return level;

}

public void setLevel(int level) {

this.level = level;

}

public String getName() {

return name;

}

public void setName(String name) {

Java SE 7技術手冊草稿

this.name = name;

}

}

接著你為魔法師定義類別:

public class Magician {

private String name; // 角色名稱角色名稱角色名稱角色名稱

private int level; // 角色等級角色等級角色等級角色等級

private int blood; // 角色血量角色血量角色血量角色血量

public void fight() {

System.out.println("魔法攻擊");

}

public void cure() {

System.out.println("魔法治療");

}

public int getBlood() {

return blood;

}

public void setBlood(int blood) {

this.blood = blood;

}

public int getLevel() {

return level;

}

public void setLevel(int level) {

this.level = level;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

}

Java SE 7技術手冊草稿

你注意到什麼呢?因為只要是遊戲中的角色,都會具有角色名稱、等級與血量,

類別中也都為名稱、等級與血量定義了取值方法與設值方法,Magician中粗體字部

份與 SwordsMan中相對應的程式碼重複了。重複在程式設計上重複在程式設計上重複在程式設計上重複在程式設計上,,,,就是就是就是就是不好的訊號不好的訊號不好的訊號不好的訊號。

舉個例子來說,如果你要將 name、level、blood 改名為其它名稱,那就要修改

SwordsMan與 Magician兩個類別,如果有更多類別具有重複的程式碼,那就要修

改更多類別,造成維護上的不便。

如果要改進,可以把相同的程式碼提昇(Pull up)為父類別:

Game1 Role.java

package cc.openhome;

public class Role {

private String name;

private int level;

private int blood;

public int getBlood() {

return blood;

}

public void setBlood(int blood) {

this.blood = blood;

}

public int getLevel() {

return level;

}

public void setLevel(int level) {

this.level = level;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

Lab

Java SE 7技術手冊草稿

}

這個類別在定義上沒什麼特別的新語法,只不過是將 SwordsMan與 Magician

中重複的程式碼複製過來。接著 SwordsMan可以如下繼承 Role:

Game1 SwordsMan.java

package cc.openhome;

public class SwordsMan extends Role {

public void fight() {

System.out.println("揮劍攻擊");

}

}

在這邊看到了新的關鍵字 extends,這表示 SwordsMan會擴充 Role的行為,

也就是繼承 Role的行為,再擴充 Role原本沒有的 fight()行為。程式面上來說,

Role中有定義的程式碼,SwordsMan中都繼承而擁有了,並再定義了 fight()方

法的程式碼。類似地,Magician也可以如下定義繼承 Role類別:

Game1 Magician.java

package cc.openhome;

public class Magician extends Role {

public void fight() {

System.out.println("魔法攻擊");

}

public void cure() {

System.out.println("魔法治療");

}

}

SwordsMan繼承Role的行為,再擴充了Role原本沒有的fight()與cure()

行為。

Lab

Lab

Java SE 7技術手冊草稿

提示提示提示提示

在這個類別圖中,第一格中 Role 表示類別名稱,第二格中 name、

level、blood 表示資料成員,:號之後為各成員型態,-號表示

private,第三格表示方法名稱,+號表示 public,:號之後表示傳回

型態,繼承則以空心箭頭表示。

如何看出確實有繼承了呢?以下簡單的程式可以看出:

Game1 RPG.java

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);

System.out.printf("劍士:(%s, %d, %d)%n", swordsMan.getName(),

swordsMan.getLevel(), swordsMan.getBlood());

Magician magician = new Magician();

magician.setName("Monica");

Lab

Java SE 7技術手冊草稿

magician.setLevel(1);

magician.setBlood(100);

System.out.printf("魔法師:(%s, %d, %d)%n", magician.getName(),

magician.getLevel(), magician.getBlood());

}

}

雖然 SwordsMan 與 Magician 並沒有定義 getName()、getLevel()與

getBlood()等方法,但從 Role繼承了這些方法,所以就如範例中可以直接使用,

執行的結果如下: 劍士:(Justin, 1, 200) 魔法師:(Monica, 1, 100)

繼承的好處之一,就是若你要將 name、level、blood改名為其它名稱,那就

只要修改 Role.java就可以了,只要是繼承 Role的子類別都無需修改。

注意注意注意注意 有的書籍或文件會說,private 成員無法繼承,那是錯的!如果

private 成員無法繼承,那為什麼上面的範例 name、level、blood

記錄的值會顯示出來呢?private成員會被繼承,只不過子類別無法直

接存取,必須透過父類別提供的方法來存取(如果父類別願意提供存取

方法的話)。

6.1.2 多型與多型與多型與多型與 is-a

在 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();

Java SE 7技術手冊草稿

編譯器就是語法檢查器,要知道以上程式片段為何可以通過編譯,為何無法通過

編譯,就是將自己當作編譯器,檢查語法的邏輯是否正確,方式是從方式是從方式是從方式是從=號號號號右邊往左讀右邊往左讀右邊往左讀右邊往左讀:

右邊是不是一種左邊呢(右邊型態是不是左邊型態的子類別)?

圖圖圖圖 5.1 運用運用運用運用 is a關係判斷語法正確性關係判斷語法正確性關係判斷語法正確性關係判斷語法正確性

從右往左讀,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,既然你都明確要求編譯器別囉嗦了,編譯器就讓這段程式碼通過編譯

了,不過後果得自行負責不過後果得自行負責不過後果得自行負責不過後果得自行負責!

Java SE 7技術手冊草稿

以上面這個程式片段來說,role1確實參考至 SwordsMan實例,所以在第二行

讓 SwordsMan實例扮演扮演扮演扮演 SwordsMan並沒有什麼問題,所以執行時期並不會出錯。

圖圖圖圖 5.2 判斷是否可扮演判斷是否可扮演判斷是否可扮演判斷是否可扮演((((CAST))))成功成功成功成功

但是以下的程式片段,編譯可以成功,但執行時期會出錯:

Role role2 = new Magician();

SwordsMan swordsMan = (SwordsMan) role2;

對於第一行,Magician是一種 Role,可以通過編譯,對於第二行,role2為

Role 型態,編譯器原本認定 Role 不一定是一種 SwordsMan 而想要囉嗦,但是你

明確告訴編譯器,就是要讓 Role 扮演為 SwordsMan,所以編譯器就讓你通過編譯

了,不過後果自負,實際上,role2 參考的是 Magician,你要讓魔法師假扮為劍

士?這在執行上會是個錯誤,JVM會拋出 java.lang.ClassCastException。

圖圖圖圖 5.3扮演扮演扮演扮演((((CAST))))失敗失敗失敗失敗,,,,執行時執行時執行時執行時拋出拋出拋出拋出 ClassCastException

使用有一種(is-a)原則,你就可以判斷,何時編譯成功,何時編譯失敗,以及

Java SE 7技術手冊草稿

將扮演(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,你就可以如下設計方法並呼叫:

Java SE 7技術手冊草稿

Game2 RPG.java

package cc.openhome;

public class RPG {

public static void showBlood(Role role) {

System.out.printf("%s 血量 %d%n",

role.getName(), role.getBlood());

}

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);

}

}

在這邊僅定義了一個 showBlood()方法,參數宣告為 Role型態�,第一次呼

叫 showBlood()時傳入了 SwordsMan 實例,這是合法的語法,因為 SwordsMan

是一種 Role�,第一次呼叫 showBlood()時傳入了 Magician實例也是可行,因

為 Magician是一種 Role�。執行的結果如下:

Justin 血量 200

Monica 血量 100

這樣的寫法好處為何?就算有 100種角色,只要它們都是繼承 Role,都可以使

用這個方法顯示角色的血量,而不需要像先前重載的方式,為不同角色寫 100 個方

法,多型的寫法顯然具有更高的可維護性。

什麼叫多型?以抽象講法解釋,就是使用單一使用單一使用單一使用單一介面介面介面介面操作操作操作操作多種型態多種型態多種型態多種型態的物件的物件的物件的物件!若用以

上的範例來理解,在 showBlood()方法中,既可以透過 Role型態操作 SwordsMan

物件,也可以透過 Role型態操作 Magician物件。

注意注意注意注意 稍後會學到 Java中 interface的使用,在多型定義中,使用單一介面

操作多種型態的物件,這邊的介面並不是專指 Java 中的 interface,

����宣告為宣告為宣告為宣告為 Role型態型態型態型態

����SwordsMan是一種是一種是一種是一種 Role

����Magician是一種是一種是一種是一種 Role

Java SE 7技術手冊草稿

而是指物件上可操作的方法。

6.1.3 重新重新重新重新定義行為定義行為定義行為定義行為

現在有個需求,請設計 static方法,可以播放角色攻擊動畫,你也許會這麼想,

學剛剛多型的寫法,設計個 drawFight()方法如何?

圖圖圖圖 5.4 Role沒有定義沒有定義沒有定義沒有定義 fight()方法方法方法方法

對 drawFight()方法而言,只知道傳進來的會是一種 Role物件,所以編譯器

也只能檢查你呼叫的方法,Role 是不是有定義,顯然地,Role 目前並沒有定義

fight()方法,因此編譯錯誤了。

然而你仔細觀察一下 SwordsMan與 Magician的 fight()方法,他們的方法方法方法方法

簽署簽署簽署簽署((((method signature))))都是:

public void fight()

也就是說,操作介面是相同的,只是方法實作內容不同,你可以將 fight()方

法提昇至 Role類別中定義:

Game3 Role.java

package cc.openhome;

public class Role {

...略

public void fight() {

// 子類別要重新定義子類別要重新定義子類別要重新定義子類別要重新定義 fight()的的的的實際實際實際實際行為行為行為行為 }

}

在 Role 類別中定義了 fight()方法,由於實際上角色如何攻擊,只有子類別

才知道,所以這邊的 fight()方法內容是空的,沒有任何程式碼實作。SwordsMan

繼承 Role之後,再對 fight()的行為進行定義:

Game3 SwordsMan.java

package cc.openhome;

public class SwordsMan extends Role {

Lab

Java SE 7技術手冊草稿

public void fight() {

System.out.println("揮劍攻擊揮劍攻擊揮劍攻擊揮劍攻擊");

}

}

在繼承父類別之後,定義與父類別中相同的方法簽署,但實作內容不同,這稱為

重新定義重新定義重新定義重新定義((((Override)))),因為對父類別中已定義的方法實作不滿意,所以你在子類別

中重新定義實作。Magician繼承 Role之後,也重新定義了 fight()的行為:

Game3 Magician.java

package cc.openhome;

public class Magician extends Role {

public void fight() {

System.out.println("魔法攻擊魔法攻擊魔法攻擊魔法攻擊");

}

...略

}

由於 Role現在定義了 fight()方法(雖然方法區塊中沒有程式碼實作),所以

編譯器不會找不到 Role的 fight()了,因此你可以如下撰寫:

Game3 RPG.java

package cc.openhome;

public class RPG {

public static void drawFight(Role role) {

System.out.print(role.getName());

role.fight();

}

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);

����宣告為宣告為宣告為宣告為 Role型態型態型態型態

Java SE 7技術手冊草稿

drawFight(swordsMan);

drawFight(magician);

}

}

在 fight()方法宣告了 Role型態的參數�,那方法中呼叫的,到底是 Role中

定義的 fight(),還是個別子類別中定義的 fight()呢?如果傳入 fight()的是

SwordsMan,role參數參考的就是 SwordsMan實例,操作的就是 SwordsMan上

的方法定義:

圖圖圖圖 5.5 role牌子掛在牌子掛在牌子掛在牌子掛在 SwordsMan實例實例實例實例

這就好比 role牌子掛在 SwordsMan實例身上,你要求有 role牌子的物件攻

擊,發動攻擊的物件就是 SwordsMan 實例。同樣地,如果傳入 fight()的是

Magician,role參數參考的就是 Magician實例,操作的就是 Magician上的方

法定義:

圖圖圖圖 5.6 role牌子掛在牌子掛在牌子掛在牌子掛在 Magician實例實例實例實例

所以範例最後的執行結果是:

Justin揮劍攻擊

Monica魔法攻擊

在重新定義父類別中某個方法時,子類別必須撰寫與父類別方法中相同的簽署,

然而如果疏忽打錯字了:

public class SwordsMan extends Role {

����實際操作的是實際操作的是實際操作的是實際操作的是 SwordsMan實例實例實例實例

����實際操作的是實際操作的是實際操作的是實際操作的是 Magician實例實例實例實例

Java SE 7技術手冊草稿

public void Fight() {

System.out.println("揮劍攻擊");

}

}

以這邊的例子來說,父類別中定義的是 fight(),但子類別中定義了 Fight(),

這就不是重新定義 fight()了,而是子類別新定義了一個 Fight()方法,這是合法

的方法定義,編譯器並不會發出任何錯誤訊息,你只會在運行範例時,發現為什麼

SwordsMan完全沒有攻擊。

在 JDK5 之後支援標註(Annotation),其中一個內建的標準標註就是

@Override,如果你在子類別中某個方法前標註@Override,表示要求編譯器檢

查,該方法是不是真的重新定義了父類別中某個方法,如果不是的話,就會引發編譯

錯誤。例如:

圖圖圖圖 5.7 編譯器檢查編譯器檢查編譯器檢查編譯器檢查是否真的重新定義父類別某方法是否真的重新定義父類別某方法是否真的重新定義父類別某方法是否真的重新定義父類別某方法

如果你要重新定義某方法,加上@Override,就不用擔心打錯字的問題了。關

於標註詳細語法,會在之後章節說明。

6.1.4 抽象方法抽象方法抽象方法抽象方法、、、、抽象類別抽象類別抽象類別抽象類別

上一個範例中 Role類別的定義中,fight()方法區塊中實際上沒有撰寫任何程

式碼,雖然滿足了多型需求,但會引發的問題是,你沒有任何方式強迫或提示子類別

一定要實作 fight()方法,只能口頭或在文件上告知,不過如果有人沒有傳達到、

沒有看文件或文件看漏了呢?

如果某方法區塊中真的沒有任何程式碼實作,可以使用 abstract 標示該方法

為抽象方法抽象方法抽象方法抽象方法((((Abstract method)))),該方法不用撰寫{}區塊,直接;結束即可。例如:

Game4 Role.java

package cc.openhome;

public abstract class Role {

...略

Lab

Java SE 7技術手冊草稿

public abstract void fight();

}

類別中若有方法沒有實作,並且標示為 abstract,表示這個類別定義不完整,

定義不完整的類別定義不完整的類別定義不完整的類別定義不完整的類別就不能就不能就不能就不能用來生用來生用來生用來生成實例成實例成實例成實例,這就好比設計圖不完整,不能用來生產成品

一樣。Java中規定內含抽象方法中規定內含抽象方法中規定內含抽象方法中規定內含抽象方法的類別的類別的類別的類別,,,,一定要在一定要在一定要在一定要在 class前標示前標示前標示前標示 abstract,如上

例所示,這表示這是一個定義不完整的抽象類別抽象類別抽象類別抽象類別((((Abstract class))))。如果嘗試用抽

象類別建構實例,就會引發編譯錯誤:

圖圖圖圖 5.8 不能實例化抽象類別不能實例化抽象類別不能實例化抽象類別不能實例化抽象類別

子類別如果繼承抽象類別,對於抽象方法有兩種作法,一種作法是繼續標示該方

法為 abstract(該子類別因此也是個抽象類別,必須在 class前標示 abstract),

另一個作法就是實作抽象方法。如果兩個作法都沒實施,就會引發編譯錯誤:

圖圖圖圖 5.9 沒有實作抽象方法沒有實作抽象方法沒有實作抽象方法沒有實作抽象方法

6.2 繼承語法細節繼承語法細節繼承語法細節繼承語法細節 上一節介紹了繼承的基礎觀念與語法,然而結合 Java 的特性,繼承還有許多細

節必須明瞭,像是哪些成員可以限定在子類別中使用、哪些方法簽署算重新定義、Java

中所有物件都是一種 java.lang.Object等細節,這將於本節中詳細說明。

6.2.1protected成員成員成員成員

就上一節的 RPG遊戲來說,如果建立了一個角色,想顯示角色的細節,必須如

下撰寫:

SwordsMan swordsMan = new SwordsMan();

...略

System.out.printf("劍士劍士劍士劍士 (%s, %d, %d)%n", swordsMan.getName(),

swordsMan.getLevel(), swordsMan.getBlood());

Magician magician = new Magician();

...略

System.out.printf("魔法師魔法師魔法師魔法師 (%s, %d, %d)%n", magician.getName(),