我是一個 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 才會看「物件真實型別」
這跟產品設計非常像:
- 我們在需求階段先定義「規格」
- 在開發階段,工程師會有不同的「實作」
- 系統主流程依賴的是抽象規格,而不是特定實作,才能保持靈活與擴展性