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,以及兩個子類別:Manager 與 Developer。所有員工都會工作 — 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) 搞混。多載是在同一個類別中,使用相同方法名稱但不同參數;覆寫是子類別改變基底類別方法的行為。
GO TO FULL VERSION