Overload (多載) 與 Override (覆寫) 為程式設計的 2 個常見性質,
對 物件導向程式設計 (OOP) 尤其重要。
或許是原文相似的關係,兩者時常令初學者搞混 😨,
不然就是對其了解只停留在:
「多個相同方法名稱」、「改寫父類別方法」…,而不懂實際運用。
別擔心!本篇將以簡單的例子,闡明各自用途、使用方式,
使您不再模凌兩可,並運用自如 😆。
[註]:
若無指明,本篇使用的語言預設為 Java。
目錄
Overload (多載)
在解釋 Overload (多載) 的含義之前,先來記憶名詞吧!
「 Load 」 本身有 乘載、負擔、負荷、負載 的意思。
於是,加上一個 Down,成了 Download (下載),
若加上一個 Over,則成了 — Overload (多載) 啦!
又譯為: 超載、覆載、重載、過載…「各種」載。
恭喜,您已學會分辨 Overload (多載) 與 Override (覆寫) 😂。
其實你學過了!
Overload (多載) 的觀念其實您已見過無數次 😂!
其為 Christopher Strachey 於 1967 年提出的一 特定多型 (Ad hoc polymorphism) 機制,
簡單來說就是: 根據不同情境『 相同的模樣,擁有不同的行為 』。
還記得國小數學嗎?
當時的「+」、「–」運算子,純粹就是做正數的 加法、減法。
然而上了國一,老師告訴你 負數 的觀念:「–3 + 5 = 2」,
以及如何強調一個 正數:「+10」…。
這時你發現了:
一樣都長「+」、「–」,在不同時候卻有 不同意義 (加減 vs. 正負數)。
會了 😇:
這正是 運算子多載 (Operator Overloading)!
— — 將多載觀念:『相同的模樣,擁有不同的行為』,應用在運算子上。
沒錯,Java 串接字串使用的「+」,一樣是 運算子多載 喔!
另外,可別忘記 字串 是 immutable (永不改變的) !
public class Main {
public static void main(String[] args) {
String name = "Jason";
String output = "Hello, " + name + "!";
System.out.println(output);
}
}
/*
* Output:
*
* Hello, Jason!
*/
[註]:
不同於某些語言 (e.g., C++),
Java、C… 並不 提供『自訂的運算子多載』,詳見 此篇。
需注意的是,當有人同時提及 Overload (多載) 與 Override (覆寫) 時,
87% 指的都是 方法多載 (Method Overloading),而非 運算子多載 唷 😲!
方法多載 (Method Overloading)
方法多載 (Method Overloading) or 函式/建構子多載,顧名思義:
將觀念『相同的模樣,擁有不同的行為』,應用在 方法/函式/建構元 上。
運算子的 模樣 很簡單:「 + – * / & ^ | << >>… 」,
而 『方法的模樣』便是指 — 方法名稱 (method’s name) ,
也就是: 相同的 方法名稱,擁有不同的 實作。
條件是:
參數串列 (parameter list) 得 不同
( 包含不同的參數型別、數量 )
例如:
void test();
void test(int i);
void test(char c);
void test(String s, int i);
void test(String s, String s2);
[註]:
大多數 物件導向程式語言 皆支援 方法多載 (e.g., Java, C++),
而傳統的 結構化程式語言 (e.g., C 語言) 不支援。
無多載方法
直接看個栗子 🌰
Bob 是個愛煎牛排的固執廚師 (chef):
你可以跟他點餐,但你沒有選擇 (無參數),永遠只有牛排 😂:
public class Main {
public static void main(String[] args) {
Chef bob = new Chef();
bob.cook(); // 牛排x1
}
}
// 廚師 (chef)
class Chef {
// 回傳型別: void
// 參數型別: 無
void cook() {
System.out.println("準備餐點: 牛排");
}
}
多載方法 (增加參數)
儘管 Bob 只想煎牛排,最後還是向現實屈服了:
允許顧客點餐 (增加 char 參數),
目前只提供 A號餐、B號餐,若您北爛亂點餐 Bob 會很生氣:
public class Main {
public static void main(String[] args) {
Chef bob = new Chef();
bob.cook(); // 牛排x1
bob.cook('B'); // 豬排x1
bob.cook('C'); // 食屎
}
}
class Chef {
// 回傳型別: void
// 參數型別: 無
void cook() {
System.out.println("準備餐點: 牛排");
}
// 回傳型態: void
// 參數型態: char
void cook(char meal) {
switch (meal) {
case 'A':
System.out.println("準備 A 號餐: 牛排");
break;
case 'B':
System.out.println("準備 B 號餐: 豬排");
break;
default:
System.out.println("食屎吧你");
break;
}
}
}
舉凡 遊戲 id、樂團名稱…,取名字都是一件麻煩事 😒,
方法多載 (Overload) 讓我們得以使用 相同名稱 宣告方法 😇,
而非取個 cookforOrder(char c) 或 cook2(char c)…。
感恩多載,讚嘆多載
多載方法 (多個參數)
身為一個專業的廚師,讓顧客指定數量也是很合理的 (增加 int 參數):
// 回傳型別: void
// 參數型別: char, int
void cook(char meal, int quantity) {
if (quantity < 1)
return;
switch (meal) {
case 'A':
System.out.println("準備 A 號餐: 牛排");
break;
case 'B':
System.out.println("準備 B 號餐: 豬排");
break;
default:
System.out.println("食屎吧你");
break;
}
// 做好一份了,剩下 quantity - 1 份
cook(meal, quantity - 1); // 遞迴呼叫
}
[註]:
最後一行,使用 自己呼叫自己的技巧,稱為 遞迴 (Recursion)。
執行結果:
public class Main {
public static void main(String[] args) {
Chef bob = new Chef();
bob.cook('A'); // 牛排x1
bob.cook('B', 3); // 豬排x3
}
}
有沒有好方便!儘管呼叫相同的 方法名稱,
程式會自動找到對映的 參數型別 (或無參數) 以執行。
關鍵在於:
呼叫的 參數型別、順序 或 數量 不同
cook( ) vs. cook(“A”) vs. cook(‘B’, 3);
方法簽章 (Method Signature)
整理一下,目前 廚師 (Chef) 類別 的多載方法:
class Chef {
// 回傳型別: void
// 參數型別: 無
void cook() {
... 略 ...
}
// 回傳型別: void
// 參數型別: char
void cook(char meal) {
... 略 ...
}
// 回傳型別: void
// 參數型別: char, int
void cook(char meal, int quantity) {
... 略 ...
}
}
Q1: 請問,是否能再擴充以下方法? (參數串列 與 cook(char meal)
衝突,但 修改回傳型態 )
boolean cook(char meal){
... 略 ...
}
Ans:
不行!
因為其 參數串列 與 void cook(char meal)
的 重複了!
在 Java 中:
方法簽章 (Method Signature) = 方法名稱 (method’s name) + 參數型別 (parameter types),
用以決定方法的 唯一性,其中 Signature 又譯為:外貌簽名、簽署、署名。
編譯器 是使用 方法簽章 (Method Signature) 區分不同方法,而非 回傳型別 (return type),
因此,不能宣告兩個具有相同簽章的方法。
[註]:
相同的觀念也出現在 C# 中:
方法的 傳回類型 不是 方法多載用途的方法簽章的一部分。
不過,在判斷委派與所指向的方法之間的相容性時,它是方法簽章的一部分。
然而,這也衍伸了一個問題:
Q2:請問,是否能再擴充以下方法? (參數串列 與 Chef 類別 無衝突,但 修改回傳型態 )
boolean cook(int i){
... 略 ...
}
Ans:
可以,但 最好不要!
從範例中可得知,重載方法之間雖然參數串列不同,
但 功能是一致的 — 烹飪 (cook)。
重載方法之間,若回傳型別不同,
將使程式碼 難以維護、理解,若有新的意圖應取新的方法名稱。
— — 軟體開發大師 Kent Beck.
重構 — 重複的程式碼 (Duplicated Code)
程式碼的壞味道 (Bad Smells in Code, by Kent Beck & Martin Fowler) 中,
首當其衝的便是 重複的程式碼 (Duplicated Code) !
讓我們看看,截至目前為止 Chef 類別,有多少『 準備餐點 』😱:
class Chef {
void cook() {
System.out.println("準備 A 號餐: 牛排");
}
void cook(char meal) {
switch (meal) {
case 'A':
System.out.println("準備 A 號餐: 牛排");
break;
case 'B':
System.out.println("準備 B 號餐: 豬排");
break;
default:
System.out.println("食屎吧你");
break;
}
}
void cook(char meal, int quantity) {
if (quantity < 1)
return;
switch (meal) {
case 'A':
System.out.println("準備 A 號餐: 牛排");
break;
case 'B':
System.out.println("準備 B 號餐: 豬排");
break;
default:
System.out.println("食屎吧你");
break;
}
// 做好一份了,剩下 (quantity - 1) 份
cook(meal, quantity - 1); // 遞迴呼叫
}
}
真的是臭到爆了 😂,而且第二個函式還 100% 出現在 第三個函式中…。
別擔心,由於 多載函式 大多 目的一致,
大多內建『 重複的程式碼 』之解法 — 提煉函式。
void cook()
方法,能夠 點一份牛排,
而 void cook(char meal)
方法,能 點一份牛排 或豬排,
於是我們能夠如此重構:
void cook() {
cook('A'); // 呼叫多載函式
}
void cook(char meal) {
switch (meal) {
case 'A':
System.out.println("準備 A 號餐: 牛排");
break;
case 'B':
System.out.println("準備 B 號餐: 豬排");
break;
default:
System.out.println("食屎吧你");
break;
}
}
又 void cook(char meal)
只能 點一份 牛排或豬排,
但 void cook(char meal, int quantity)
能夠 點很多份 牛排或豬排!
於是我們能夠如此重構:
void cook(char meal) {
cook(meal, 1); // 相當於數量 x1
}
最後:
class Chef {
// 點一份 牛排
void cook() {
cook('A');
}
// 點一份 牛排 or 豬排
void cook(char meal) {
cook(meal, 1);
}
// 點很多份 牛排 or 豬排
void cook(char meal, int quantity) {
if (quantity < 1)
return;
switch (meal) {
case 'A':
System.out.println("準備 A 號餐: 牛排");
break;
case 'B':
System.out.println("準備 B 號餐: 豬排");
break;
default:
System.out.println("食屎吧你");
break;
}
// 做好一份了,剩下 quantity - 1 份
cook(meal, quantity - 1); // 遞迴呼叫
}
}
是否乾淨許多呢 😆!
But,得注意的是:
對未包含共同程式或邏輯的 方法,可能並不適用,
例如,需依不同型別做個別的處理的 方法 (method)。
建構子多載 (Constructor Overloading)
建構子 (Constructor) 是一種特殊的方法,會在類別被 實例 (new) 時自動執行,
建構子 沒有回傳值,且 名稱必須與類別相同。
上方的重構技巧,便常見於『 具有多個 建構子 (Constructor) 的類別 』中,
常見的做法是:『 由簡單去呼叫複雜 』。
例如,我們要製作一個具有 長、寬、顏色 的 長方形 (Rectangle),
且若無指定值,預設是 寬500 高309 藍色 的長方形,一開始可能寫成:
class Rectangle {
int width;
int height;
int color;
Rectangle() {
this.width = 500;
// 別問我為何 1.618...去問歐幾里得
this.height = (int) (width / 1.618);
this.color = 0x2196F3; // 藍色
}
Rectangle(int width) {
this.width = width;
this.height = (int) (width / 1.618);
this.color = 0x2196F3; // 藍色
}
Rectangle(int width, int height) {
this.width = width;
this.height = height;
this.color = 0x2196F3; // 藍色
}
Rectangle(int width, int height, int color) {
this.width = width;
this.height = height;
this.color = color;
}
}
沒關係 😆,照著 廚師 (chef) 類別依樣畫葫蘆,進行重構 ❗️
class Rectangle {
int width;
int height;
int color;
Rectangle() {
this(500); // 正確 (O)
// Rectangle(width); 錯誤 (X) -- 得使用 this 關鍵字
}
Rectangle(int width) {
this(width, (int) (width / 1.618));
}
Rectangle(int width, int height) {
// int area = width * height; 錯誤 (X) -- 利用「this」呼叫建構子,需為首句敘述
this(width, height, 0x2196F3);
}
// 主建構函式
Rectangle(int width, int height, int color) {
this.width = width;
this.height = height;
this.color = color;
}
}
跟上節的重構非常相似! 但最大的不同在於:
建構子無法像方法一樣以名稱呼叫,得透過關鍵字 this 或 super (子類別),
且該 this 或 super 必須是該建構元的 首句敘述 (first statement)。
注意:
this() 和 this. 不一樣喔!
this()
用於 在 建構元中 呼叫 其他建構元,且需為 首句敘述 (first statement),
this.
用於存取類別中的欄位 (例 this.width),放第一萬行也沒關係!
最後,我們便能以簡單的方式建立長方形,也可以詳細的對其進行設定 😆:
無參數建構子:
三參數建構子:
附上 Main 程式碼:
import javax.swing.*;
import java.awt.*;
public class Main {
public static void main(String[] args) {
Rectangle defaultRec = new Rectangle();
showJFrame(defaultRec);
}
static JFrame showJFrame(Rectangle r) {
int width = r.width;
int height = r.height;
Color color = new Color(r.color);
// 簡易的 Swing 圖形化介面
JFrame frame = new JFrame();
frame.setSize(width, height);
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.getContentPane().setBackground(color);
frame.setVisible(true);
return frame;
}
}
以上的作法,便是 軟體開發大師 Kent Beck 提及:
將所有建構函式,轉接道一個 『 主建構函式 』,
以向未來的維護/修改者,傳達這些『 不會改變的要求 』。
子類別的多載
最後,得注意的是:
『 多載不只會發身在自身類別,繼承/實作 的子類別同樣能多載父類別的方法! 』
public class Main {
public static void main(String[] args) {
new A().test(); // A
new B().test(); // B
new B().test('C'); // C
}
}
class A {
void test() {
System.out.println('A');
}
}
class B extends A {
// 覆寫 (Override)
void test() {
System.out.println('B');
}
// 多載 (Overload)
void test(char c) {
System.out.println(c);
}
}
然而,可別忘了多載的條件是:
參數串列 (parameter list) 得 不同
( 包含不同的參數型別、數量 )
若 方法簽章 (方法名稱+參數串列) 與 父類別 相同,
並非多載,而是 Override (覆寫) — 會將父類別的方法覆蓋掉 (改寫),
詳細觀念將於下篇提及 😄。
總結
多載 (Overlaod) 大幅地增加了程式開發的便利性,
例如,Java 的 valueOf
函式,提供了多種 多載方法,
使我們得以用相同的方法名稱,執行不同資料型別的轉型操作:
然而,Overload (多載) 與 Override (覆寫) 其實是息息相關的,
皆是實踐 多型 (polymorphism) 的技術之一,
善用這些技巧,才能有效實作彈性、可擴充的程式,
Override (覆寫) 的觀念將在 下篇 提及 😁。
在《Overload (多載) vs. Override (覆寫) — (I)》中有 13 則留言
獲益良多,非常感謝,是個非常優秀的老師😁
受益匪淺,感謝分享。
感謝大神
這個很有用,感謝~~~~
真的很實用!感謝您
解釋的詳細又清楚,謝謝你~
謝謝你們 😃
感谢分享,例子非常简单易懂
謝謝 😄
謝謝!講的很詳細 :
非常感謝!😆
例子淺顯易懂 講解得非常棒 謝謝您的說明!
寫得還不錯咧臭小子~