設計模式/原則

Overload (多載) vs. Override (覆寫) — (I)

Overload (多載)Override (覆寫) 為程式設計的 2 個常見性質,
物件導向程式設計 (OOP) 尤其重要。
 
或許是原文相似的關係,兩者時常令初學者搞混 😨,
不然就是對其了解只停留在:
「多個相同方法名稱」、「改寫父類別方法」…,而不懂實際運用。
 
別擔心!本篇將以簡單的例子,闡明各自用途、使用方式,
使您不再模凌兩可,並運用自如 😆。

 
[註]:
若無指明,本篇使用的語言預設為 Java。
 


 

Overload (多載)

在解釋 Overload (多載) 的含義之前,先來記憶名詞吧!
 

Load 」 本身有 乘載、負擔、負荷、負載 的意思。

 
於是,加上一個 Down,成了 Download (下載),
若加上一個 Over,則成了 — Overload (多載) 啦!
又譯為: 超載、覆載、重載、過載…「各種」載。
 
恭喜,您已學會分辨 Overload (多載)Override (覆寫) 😂。
 
overload-vs-override
 


 

其實你學過了!

Overload (多載) 的觀念其實您已見過無數次 😂!
其為 Christopher Strachey 於 1967 年提出的一 特定多型 (Ad hoc polymorphism) 機制,
簡單來說就是: 根據不同情境『 相同的模樣,擁有不同的行為 』。
 
 
還記得國小數學嗎?
當時的「+」、「」運算子,純粹就是做正數的 加法、減法
add-sub-stract-multiply-divide
 
然而上了國一,老師告訴你 負數 的觀念:「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 則留言

發表迴響