Java 學習筆記 01:Java 的多型與類別轉型

Java 學習筆記 01:Java 的多型與類別轉型

2025-08-22

作為一個 PM,在學習 Java 的過程中,我最卡的地方是「多型」與「類別轉型」。這篇筆記整理了我的理解:什麼是多型、向上/向下轉型的差別,以及為什麼 a.fetch() 在編譯時不行但執行時可行。透過支付系統、集合框架、遊戲角色的實例,我逐漸體會到多型的實際價值 —— 先依賴抽象,再由執行時決定具體實作。

我是一個 PM,平常大部分時間都在做產品規劃、需求拆解、跨部門協調 但隨著產品越來越複雜,我發現自己對後端架構的理解不足,常常無法和工程師討論更進一步的設計選項 於是我開始自學 Java,希望能對一些後端架構設計(特別是物件導向、抽象化、多型)有更深的感覺

這篇就是我的學習筆記,整理我在學習 多型(Polymorphism) 與 類別轉型(Casting) 時的心得

多型(Polymorphism)是什麼?

先看一個簡單的例子:

class Animal {
    void makeSound() {
        System.out.println("Some generic animal sound");
    }
}

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

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

如果我寫一個方法:

public static void playWithAnimal(Animal animal) {
    animal.makeSound();
}

呼叫的時候:

playWithAnimal(new Dog()); // 輸出 "Woof!"
playWithAnimal(new Cat()); // 輸出 "Meow!"

雖然程式碼裡同樣是 animal.makeSound(),但實際輸出會依照「物件真實的型別」來決定 👉 這就是多型的核心:同一份程式碼,執行時依照物件不同,行為不同 對我來說,這就像是「定義一份規格書(父類/介面),不同實作(子類)在執行時會各自表現」 這意味著,只要我前期把抽象設計好,後期增加新功能時,主流程幾乎不用動

向上轉型(Upcasting)

多型的基礎是 向上轉型,也就是「子類別 → 父類別」

Animal a = new Dog(); // 向上轉型
a.makeSound();        // 輸出 "Woof!"

這裡 a 是 Animal,但實際上指向一個 Dog

  • 編譯時:編譯器只知道 a 是 Animal,所以允許呼叫 Animal 定義的方法
  • 執行時:JVM 發現 a 的實際型別是 Dog,於是執行 Dog 的版本 👉 向上轉型是安全的,而且幾乎自動發生。它讓我們可以用「通用規格」處理不同的子類別

向下轉型(Downcasting)

如果我想呼叫 Dog 特有的方法(例如 fetch()),就遇到問題了:

Animal a = new Dog();
a.fetch(); // ❌ 編譯錯誤,因為 Animal 沒有 fetch()

編譯器只認為 a 是 Animal,即使執行時它其實是一隻狗,也不允許你直接呼叫。 這時候需要「向下轉型」:

Dog d = (Dog) a; // 向下轉型
d.fetch();       // ✅ 可以呼叫 Dog 特有的方法

⚠️ 但要注意,向下轉型不保證安全。如果物件不是 Dog,就會丟 ClassCastException:

Animal b = new Animal();
Dog d2 = (Dog) b;  // ❌ 執行期錯誤

所以實務上通常會搭配 instanceof 檢查:

if (a instanceof Dog) {
    ((Dog) a).fetch();
}

我卡住的點:編譯時型別 vs 執行時型別

我自己花了很久才理解的一個問題是:

在編譯的時候 a 叫做 Animal,運行的時候 a 叫做 Dog。 那我直接寫 a.fetch() 在運行上應該是可行的,為什麼編譯器卻不讓我過? ? 為什麼一定要寫 Dog d = (Dog) a; 才能呼叫?

答案是:編譯器只看「編譯時型別」,而不會去猜「執行時型別」。

  • 編譯時:a 的型別 = Animal,Animal 沒有 fetch() → 編譯錯誤。
  • 執行時:a 的物件真的是 Dog,JVM 的確能執行 fetch(),但程式根本沒被允許編譯過。 所以才需要這行:
Dog d = (Dog) a;

這等於告訴編譯器:「我保證這其實是一隻 Dog,請當成 Dog 來看待。」 這樣編譯器就允許你呼叫 fetch() 這也是多型的一個設計哲學: 👉 編譯期靠「抽象」保證安全,執行期靠「實際型別」決定行為

多型的實際用途

理解原理之後,我更關心它的應用價值,其中一個案例就是支付系統:

abstract class Payment {
    abstract void pay(int amount);
}
class CreditCard extends Payment { ... }
class Paypal extends Payment { ... }
class ApplePay extends Payment { ... }

結帳流程:

public void checkout(Payment payment, int amount) {
    payment.pay(amount);
}

呼叫時:

checkout(new CreditCard(), 100);
checkout(new Paypal(), 200);

👉 方法只依賴 Payment 抽象,不在乎具體實作。未來要新增 GooglePay 也不用改動現有程式

總結

理解多型與類別轉型,不只是學 Java 語法,而是進一步體會到「設計抽象的重要性」

  • 多型:讓我可以先設計抽象規格(Animal / Payment / List),執行時才由具體實作(Dog / Paypal / ArrayList)決定行為
  • 向上轉型:保證通用性,用抽象來接住不同實作
  • 向下轉型:在需要特殊功能時,把抽象還原成具體
  • 編譯 vs 執行:編譯器只認「型別宣告」,JVM 才會看「物件真實型別」

這跟產品設計非常像:

  • 我們在需求階段先定義「規格」
  • 在開發階段,工程師會有不同的「實作」
  • 系統主流程依賴的是抽象規格,而不是特定實作,才能保持靈活與擴展性