CodeGym /課程 /JAVA 25 SELF /多型(Polymorphism)的概念與用途

多型(Polymorphism)的概念與用途

JAVA 25 SELF
等級 18 , 課堂 0
開放

1. 前言

多型 是物件導向程式設計的三大核心概念之一(與繼承、封裝並列)。字面上源自希臘語的「poly」(多)與「morph」(形)。在程式設計中,它的意思是:一個介面,多個實作

定義

多型 是指不同類別的物件能對相同的訊息(方法呼叫)以不同方式做出回應的能力。

也就是說,如果你有一個方法 makeSound(),你可以對任何動物呼叫它,但貓會喵叫、狗會汪叫、牛會哞叫。對程式設計師而言,這只是呼叫 animal.makeSound();而實際會發生什麼,取決於該變數背後實際指向的是哪一個物件。

生活中的類比

想像你家裡有一支遙控器,不只可以操控電視,還能控制喇叭、投影機,甚至咖啡機。你按下「開機」按鈕 — turnOn(),每個裝置都會用自己的方式回應。重點是:大家都有「開機」這個操作,但實作各不相同。

Java 範例

class Animal {
    void makeSound() {
        System.out.println("某種聲音...");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("汪!");
    }
}

class Cat extends Animal {
    @Override
    void makeSound() {
        System.out.println("喵!");
    }
}

class Cow extends Animal {
    @Override
    void makeSound() {
        System.out.println("哞!");
    }
}

現在我們可以這樣做:

Animal animal1 = new Dog();
Animal animal2 = new Cat();
Animal animal3 = new Cow();

animal1.makeSound(); // 汪!
animal2.makeSound(); // 喵!
animal3.makeSound(); // 哞!

請注意:所有變數的型別都是 Animal,但實際呼叫的結果取決於物件的實際型別。

2. 多型的種類

在 Java(以及大多數 OOP 語言)中,主要有兩種多型:

編譯期(靜態)多型 — 方法多載(overloading)

也就是在同一個類別中,有多個方法名稱相同,但參數不同。編譯器會根據傳入的引數決定要呼叫哪個方法。

範例(先小小劇透——詳細內容在下一堂課):

class Printer {
    void print(int x) {
        System.out.println("數字: " + x);
    }

    void print(String s) {
        System.out.println("字串: " + s);
    }
}

執行期(動態)多型 — 方法覆寫(overriding)

也就是在基底類別中定義的方法,會在子類別中被覆寫。實際會呼叫哪個方法,是在程式執行期(runtime)依物件的實際型別決定。

範例——見上面的動物例子。

3. 為什麼需要多型?

多型 不只是面試時的漂亮名詞,它是讓你的程式碼更有彈性、可擴充、且容易維護的工具。

程式碼的通用性

你可以撰寫只面向基底型別的程式碼,而不必在意具體實作的細節。例如,若你有一個動物清單,你可以逐一呼叫每個物件的 makeSound(),不需要管它是貓還是狗。

Animal[] animals = { new Dog(), new Cat(), new Cow() };

for (Animal animal : animals) {
    animal.makeSound(); // 每次都會呼叫「正確」的方法
}

易於擴充

如果明天老闆說:「我們加一隻鸚鵡吧!」你只要寫一個新的類別 Parrot extends Animal,然後把它加進陣列,其餘程式碼都不需要改。這就是 對擴充開放、對修改封閉(SOLID 中的 OCP 原則)。

簡化架構

你可以構建複雜系統,讓各個部分透過抽象(基底類別或介面)彼此互動,而不需要關心具體實作。這能省下時間、精力與咖啡。

4. 關鍵概念:參考型別與實際型別

變數的參考型別
當你寫下 Animal animal = new Dog(); 時,變數 animal 的參考型別是 Animal。也就是說,編譯器「認為」它是動物,並且只允許呼叫在 Animal 類別中宣告的方法。

物件的實際(真實)型別
但在記憶體中,實際存在的是一個 Dog 物件。當你呼叫 makeSound() 時,真正會執行的方法由它來決定。

示意

Animal animal = new Dog();
animal.makeSound(); // 會呼叫 Dog.makeSound(),而不是 Animal.makeSound()

重要! 透過基底型別(Animal)的參考,你無法呼叫只有 Dog 才有、且未在基底類別中宣告的方法。

延遲(動態)繫結
這種「魔法」發生在執行期:當你透過基底型別的參考呼叫方法時,JVM 會檢查物件的實際型別,並呼叫「正確」的實作。這就是 多型的實際運作

5. 實作範例:應用程式中的多型

讓我們延續教學應用的範例。假設我們在寫一個簡單的動物園模擬。我們有一個基底類別 Animal,以及它的幾個子類別。我們希望所有動物都能「發出聲音」,但不想每種動物都寫一份重複的程式碼。

步驟 1:基底類別與子類別

class Animal {
    void makeSound() {
        System.out.println("某種聲音...");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("汪!");
    }
}

class Cat extends Animal {
    @Override
    void makeSound() {
        System.out.println("喵!");
    }
}

步驟 2:動物陣列

Animal[] zoo = { new Dog(), new Cat(), new Animal() };

步驟 3:迭代並呼叫方法

for (Animal animal : zoo) {
    animal.makeSound();
}

執行結果:

汪!
喵!
某種聲音...

請注意:我們事先不知道陣列裡的是誰——但程式會自行判斷並呼叫需要的方法。

6. 多型的示意圖


.        Animal (makeSound)
           /        \
        Dog        Cat
     (makeSound) (makeSound)

Animal animal = new Dog();
animal.makeSound(); // --> Dog.makeSound()

Animal animal = new Cat();
animal.makeSound(); // --> Cat.makeSound()

7. 再一個例子:實務情境中的多型

假設你在開發一個公司員工管理程式。你有一個基底類別 Employee,以及兩個子類別:ManagerDeveloper。所有員工都會工作 — work(),但作法不同。

class Employee {
    void work() {
        System.out.println("員工正在工作。");
    }
}

class Manager extends Employee {
    @Override
    void work() {
        System.out.println("經理正在開會。");
    }
}

class Developer extends Employee {
    @Override
    void work() {
        System.out.println("開發者正在寫程式碼。");
    }
}

現在你可以這樣做:

Employee[] staff = { new Manager(), new Developer(), new Employee() };

for (Employee emp : staff) {
    emp.work();
}

結果:

經理正在開會。
開發者正在寫程式碼。
員工正在工作。

8. 什麼時候多型不適用

多型只對在基底類別中宣告的方法有效。如果子類別有自己獨有的方法,透過基底型別的參考是看不到的。

class Dog extends Animal {
    void fetchStick() {
        System.out.println("狗把木棍叼回來!");
    }
}

Animal animal = new Dog();
// animal.fetchStick(); // 編譯錯誤!透過 Animal 看不到這個方法

若要呼叫子類別的專屬方法,需要將變數轉型為對應的型別:

if (animal instanceof Dog) {
    ((Dog) animal).fetchStick();
}

但這又是另一個主題——重點是:透過多型只能使用基底類別中宣告的方法。

9. 使用多型時的常見錯誤

錯誤 1: 期望能透過基底型別的參考呼叫到子類別的所有方法。事實上只能使用基底類別中宣告的方法。

錯誤 2: 覆寫方法時沒有使用 @Override 註解。少了它,可能誤寫成簽章不相同的方法,導致覆寫沒有生效(也就不會觸發多型)。

錯誤 3: 未轉型就呼叫子類別的專屬方法。編譯器不會允許,因為它不知道基底型別參考實際指向的是誰。

錯誤 4:多載(overloading)覆寫(overriding) 搞混。多載是在同一個類別中,使用相同方法名稱但不同參數;覆寫是子類別改變基底類別方法的行為。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION