在 上篇 中,介紹了 Overload (多載) 的種類及實作技巧,
接下來則要討論 — Override (覆寫)!
儘管強調兩者的差異,它們仍息息相關 🤔,
皆是實踐 多型 (polymorphism) 的技術之一,
善用這些技巧,才能有效實作彈性、可擴充的程式!
在開始之前,強烈建議閱讀 依賴倒置原則 (DIP),
您將能對 多型、抽象、介面…有進一步的認識 😁。
[註]:
若無指明,本篇使用的語言預設為 Java。
目錄
如果沒有 Override (覆寫)
先別管 Override (覆寫) 了,你聽過 吱吱喳 嗎?
這是一隻會發出吱吱叫的 小鼠 🐿 (父類別
):
class Mouse {
// 省略成員變數 (一些特徵如 眼睛、鼻子、牙齒...)
// call (V.) 動物叫
void call() {
System.out.println("吱吱喳");
}
}
有天,誕生了一隻基因突變的怪老鼠,稱為 皮卡丘 (子類別
),
他不再吱吱叫,而是發出『皮卡~ 皮卡~』:
class Pikachu extends Mouse {
// 保有父類別的成員變數
void pika() {
System.out.println("皮卡~皮卡~");
}
}
型別比較運算子 (instanceof)
無聊的我 🕺,為了比較不同 Mouse 間的叫聲,
在高階模組 Main
中,撰寫一方法 testMouse(Mouse mouse)
。
實作方式很簡單!使用 型別比較運算子 — instanceof,
即可得知傳遞進來的『 mouse 型別 』,究竟是否為 皮卡丘:
static void testMouse(Mouse mouse) {
if (mouse instanceof Pikachu) {
((Pikachu) mouse).pika(); // 是皮卡丘,則 轉型並呼叫 pika() 方法
} else {
mouse.call(); // 不是皮卡丘,則 直接呼叫 call() 方法
}
}
實際 Run Run 看:
public class Main {
public static void main(String[] args) {
Pikachu 皮卡丘 = new Pikachu();
testMouse(皮卡丘); // 將 皮卡丘 丟進實驗室
}
}
Output: 皮卡~皮卡~
大成功 😇!
Q:
testMouse(Mouse mouse)
方法要求的「參數型別」明明是 Mouse,
為何能將 Pikachu 傳進去?
Ans:
透過 多型 (polymorphism) 機制,
我們能以 父類別/介面 操作其 子類別;反之則無法。
而 Pikachu 繼承自 Mouse。
if-else 長鍊
某天,又演化出一種 頭頂局部燙傷 的老鼠,稱為 哈姆太郎 (Mouse的子類別
):
他的特色是:一興奮時,就會發出『 Heke 』的聲響
class Hamutaro extends Mouse {
void excite() {
System.out.println("Heke~");
}
}
別忘了!由於我們擴充了 子類別
,
高階模組 的 testMouse(Mouse mouse)
也 被迫 必須修改 :
static void testMouse(Mouse mouse) {
if (mouse instanceof Pikachu) {
((Pikachu) mouse).pika();
} else if (mouse instanceof Hamutaro) {
((Hamutaro) mouse).excite(); // 是哈姆太郎,則 轉型並呼叫 excite() 方法
} else {
mouse.call();
}
}
實際 Run Run 看:
public class Main {
public static void main(String[] args) {
Hamutaro 哈姆太郎 = new Hamutaro();
testMouse(哈姆太郎);
}
}
Output: Heke~
大成功 😇!……….嗎?
您是否發現 目前的做法:
每擴充一種 子類別,高階模組 的
testMouse()
方法,就被迫跟著改變
試想一下,若繼續擴充 米老鼠、傑利鼠、雷丘、拉達、地鼠、土撥鼠…,
您的 testMouse()
方法 將長成這副德性:
static void testMouse(Mouse mouse) {
if (mouse instanceof Pikachu) {
((Pikachu) mouse).pika();
} else if (mouse instanceof Hamutaro) {
((Hamutaro) mouse).excite();
} else if (mouse instanceof Mickey) {
((Mickey) mouse).xxxx();
} else if (mouse instanceof Jerry) {
((Jerry) mouse).yyyy();
} else if (mouse instanceof Raichu) {
((Raichu) mouse).zzzz();
} else {
mouse.call();
}
}
這就是聲名狼藉的 if-else 長鍊,
因此,這似乎不是什麼好的擴充方式 🤔…。
Liskov 替代原則
對不起,我講的太過委婉了,
上述 所有的程式碼,不只不是好方式,根本就 😂:
你可能會覺得:擴充 不過就加個 if-else 而已,有這麼誇張嗎?
Ans:
絕對有 😂。
若只有 Main
類別使用到 Mouse
倒還好,
但若有 多個類別 或 多個方法 使用到低階模組 Mouse
,不是改到死 就是 漏東漏西 🙀。
而之所以造成上述的種種問題,一切皆因 違反 Liskov 替代原則。
何謂 子類別 (subclass)?
Liskov 替代原則 告訴我們:
子型別 必須可以 替換 (substitute) 他們的父型別。
要讓 父類別 Mouse 發出聲音,可以透過 call()
方法,
但要讓 子類別 Pikachu 發出聲音,卻得修改 成使用 pika()
方法。
『 卻得修改 』便是 — — 不可替換。
[註]:
雖然 Pikachu 一樣能使用 call()
方法,但 結果 (後置條件) 並非預期,
除非你想要只會吱吱喳的皮卡丘 😂。
於是,一個非常非常非常重要的結論:
子型別 真正的定義是 — — 可替換的 (substitutable)。
軟體開發大師 Robert C. Martin:
正是因為 子型別的 可替換性,讓以基礎型別表達的模組得以 不需修改而加以擴充。
Override (覆寫)
大師 Kent Beck 告訴我們:
子類別 傳遞的資訊應該是『 我 和 超(父)類別 很像,只有 少許差異 』。
Pikachu 繼承了 Mouse,因此能共享其部分的結構或行為:
1. 成員變數 (一些特徵如 眼睛、鼻子、牙齒…)
2. 方法 (call、eat、drink)
然而,必須 挑出其中的 少許差異 並加以修改,
才能使之成為 可替換的 (substitutable) 子類別。
而 Override (覆寫) 便是 修改那些差異 的主要機制之一 😎:
Override (覆寫) 讓 子類別 能以異於 父類別 的方式處理訊息。
當然,就像跟朋友借作業抄之前,你得要有朋友。
子類別 要能 Override (覆寫),前提是有 父類別/介面。
[註]:
Overload (多載),則不需要。
重新定義
Override (覆寫),又譯為:重寫、改寫,
有「 撤銷,推翻;使無效 」之意。
簡而言之,就是:
重新定義
用於:
使 子類別 覆寫 (重新定義)『 繼承/實作自 父類別/介面 的方法 』。
於是,子類別 (Pikachu
) 只需 覆寫 (重新定義)『 繼承自 父類別 (Mouse
) 的 方法 (call
) 』,
便能同樣透過 call()
方法,達成預期的結果!
class Pikachu extends Mouse {
@Override
void call() {
System.out.println("皮卡~皮卡~");
}
}
此時 Pikachu 的 call()
方法已經 重新定義,
若執行該方法,輸出結果將為『 皮卡~皮卡~ 』,而非 繼承自父類別的『 吱吱喳 』:
public class Main {
public static void main(String[] args) {
Pikachu 皮卡丘 = new Pikachu();
皮卡丘.call();
}
}
Output: 皮卡~皮卡~
[註1]:
上例的 標註 (Annotations) — @Override,只是一個好習慣,而非規定!
告知編譯器,此方法試圖 覆寫 父類別方法,
並幫助開發時釐清 方法 的類別層級。
[註2]:
當初為何翻譯為 覆『寫』,我也相當好奇 😂,
可能是 ride 跟 write 唸法 87% 像 🤔?
又或者只是一種 意譯 (paraphrase)。
再見了,if-else
由於確保了 子類別 的行為與 父類別一致,
高階模組 Main
中的 testMouse(Mouse mouse)
方法,
不必 再使用 型別比較運算子 — instanceof 😇!
public class Main {
public static void main(String[] args) {
Pikachu 皮卡丘 = new Pikachu();
testMouse(皮卡丘);
}
static void testMouse(Mouse mouse) {
mouse.call();
}
}
Output: 皮卡~皮卡~
Q:
若保持此原則,繼續擴充 米老鼠、傑利鼠、雷丘、拉達、地鼠、土撥鼠…?
Ans:
testMouse(Mouse mouse)
永遠就是長這樣唷 😉:
static void testMouse(Mouse mouse) {
mouse.call();
}
因此可知,若所有子類別皆具備 可替換性 (顯式 or 隱含的符合父類別之方法契約),
結果就是 高階模組的 再利用 變得相當簡單 😇。
再見了,instanceof
再見了,冗餘的轉型
再見了,低能的 if-else
[註]:
若您的程式碼仍充滿了 if-else + instanceof,
往往代表未能善用 多型 及 沒有優良的繼承體系。
方法簽章 (Method-Signature)
在上一篇中,提及了 方法簽章 (Method Signature) 的觀念:
方法簽章 (Method Signature) = 方法名稱 (method’s name) + 參數型別 (parameter types),
用以決定方法的 唯一性,其中 Signature 又譯為:外貌簽名、簽署、署名。
並不包含 回傳型別 (return type)。
同上述範例,Override (覆寫) 的使用方式就是:
子類別 定義一個『 與 父類別/介面 相同 方法簽章 』的方法。
例如,這是一個標準的 Override (覆寫) 範例,
其中,子類別 SubClass 與 父類別 SuperClass 的 earn(int i)
方法:
方法簽章 (方法名稱 + 參數型別) 完全相同。
public class Main {
public static void main(String[] args) {
new SubClass().earn(500);
}
}
class SuperClass {
void earn(int i) {
System.out.println("老爸賺了: " + i);
}
}
class SubClass extends SuperClass {
@Override
void earn(int i) { // 方法名稱 與 參數型別 同父類別
System.out.println("兒子賺了: " + i);
}
}
Output: 兒子賺了: 500
如果沒有 覆寫 呢?
public class Main {
public static void main(String[] args) {
new SubClass().earn(500);
}
}
class SuperClass {
void earn(int i) {
System.out.println("老爸賺了: " + i);
}
}
class SubClass extends SuperClass {
// 未覆寫方法
}
Output:
老爸賺了: 500
似乎哪裡怪怪的 🤔?
爸,我 super 對不起你
上方的範例,有一個 很大的問題:
不是兒子賺錢,就是老爸賺錢 😂!
雖然,有些父親希望兒子爭氣,甚至會因 ↓ 感到失望:
Photo by Movie Inception.
然而:
對不起了 爸,請您把錢給我 😂。
要取得老爸一切成就的鑰匙 🔑,就是 — — super 關鍵字 😈,
當然,前提是 父類別 該方法存取等級 並非 private:
public class Main {
public static void main(String[] args) {
new SubClass().earn(500);
}
}
class SuperClass {
void earn(int i) {
System.out.println("老爸賺了: " + i);
}
}
class SubClass extends SuperClass {
@Override
void earn(int i) {
super.earn(100000); // 呼叫父類別方法
System.out.println("兒子賺了: " + i);
}
// 不需要是 覆寫方法 (Overriding Method) 也行:
void test() {
super.earn(1000); // 呼叫父類別方法
}
}
Output: 老爸賺了: 100000 兒子賺了: 500
感恩老爸,讚嘆老爸 🙇
由此可知:
Override (覆寫) 除了能 重新定義 父類別方法,
還能透過 super 關鍵字,以原先的方法為基礎,加以 擴充!
總結
還記得文初所述嗎:
透過 多型 (polymorphism) 機制,
我們能以 父類別/介面 操作其 子類別;反之則無法。
事實上,Override (覆寫) 的好處,務必配合 多型 才能有效發揮,
否則,可說是 沒有任何意義。
因此,目前的程式碼依舊 😂:
礙於篇幅,本篇僅闡述 覆寫 解決的問題、功用 及 效益,
實戰 與 多型 的用法就下篇再談囉 😃。
在《Overload (多載) vs. Override (覆寫) — (II)》中有 12 則留言
好我會期待下一篇的實戰與多型🤓🤓🤓
那別忘了訂閱本站!
因為會等很久喔 😂
(抱歉,這幾個月超忙 😭)
拜讀你的文章時,讓人在 樂與苦 中學習,歡樂間獲益良多。感恩師傅 讚嘆師傅~~
哈哈哈 謝謝 kevin 師兄 😂
你的文章讓人獲益良多ㄟ!! 而且又用幽默好笑的方式 ~~大神感謝你阿
太厲害 這一定要給支持
寫得太好
有点猛阿大佬,这讲的又好笑又易懂
實戰與多型的用法出來了嗎@@?
真的講得太好了
大推這個網站!!
非常期待實戰與多型的用法!
讚啦