Java 繼承與 protected 關鍵字,還有 Upcasting 入門【Thinking in Java筆記(6-3)】

Java 繼承與 protected 關鍵字,還有 Upcasting 入門【Thinking in Java筆記(6-3)】
Photo by Thais Lima / Unsplash

你有沒有想過,為什麼程式設計時我們需要「父類別/子類別」的概念?

  • 繼承:就好比爸爸的特質會傳給小孩,子類別自動擁有父類別的屬性與功能。
  • 多型:透過向上轉型(Upcasting,我們可以用同一個介面,操作不同類型的物件。

讓我們用最簡單的方式,從 為什麼要學 開始聊起

為什麼要學繼承?

想像你在辦公室裡,要處理各種動物。你不想為每一種動物都重寫 eat()sleep()。這時候,繼承就像是把動物通用行為寫在一張父親清單上,大家一樣照著做,少了重複工作,也方便管理

想像如果每個動物寫一份吃、睡方法,程式碼會變成什麼樣子?

這就是繼承的價值:

  1. 減少重複:共用父類別功能
  2. 統一管理:父類別改了,子類別全同步
  3. 結構清晰:想像家譜圖,誰是誰的子代,一目了然

protected:家族內部的小祕密

當我們想把某些資料對外隱藏,又允許子類別使用,就需要 protected

  • 對外部世界來說,protected成員就像上鎖的門,不能直接進入
  • 但同一家族(父類別和子類別),以及同個包裝(package)裡的其他類別,拿到鑰匙可以進去
class Villain {
  private String name;
  protected void set(String nm) { name = nm; }
  public Villain(String name) { this.name = name; }
  public String toString() {
    return "I'm a Villain and my name is " + name;
  }
}	

public class Orc extends Villain {
  private int orcNumber;
  public Orc(String name, int orcNumber) {
    super(name);
    this.orcNumber = orcNumber;
  }
  public void change(String name, int orcNumber) {
    set(name); // Available because it's protected
    this.orcNumber = orcNumber;
  }
  public String toString() {
    return "Orc " + orcNumber + ": " + super.toString();
  }	
  public static void main(String[] args) {
    Orc orc = new Orc("Limburger", 12);
    print(orc);
    orc.change("Bob", 19);
    print(orc);
  }
} /* Output:
Orc 12: I'm a Villain and my name is Limburger
Orc 19: I'm a Villain and my name is Bob
*/

想像你家有一間密室,只有家族成員能進去拿工具、調整傢俱;訪客則只能在門口觀望,無法改動任何東西

再想像每個寄出的包裹,都有一把專屬鑰匙,只有寄件人和收件人能打開;其他人既看不到也改不了裡面的物品。這就像 protected,僅允許「家族」成員或同一包裝內的類別訪問

  • Villain 類別有一個 protected 方法 set(String nm),子類別 Orc 可以訪問並使用這個方法
  • Orcchange() 方法使用了 set() 來修改 name,並修改了自己的 orcNumber
  • OrctoString() 方法覆寫了 VillaintoString(),並透過 super.toString() 調用父類別的方法

這展示了 protected 如何允許子類別訪問父類別的成員,同時仍然對其他類別保持隱藏

Upcasting:專用到通用的魔法

想像你擁有一把 風笛Wind),但調音師的工作單上只寫了「樂器」(Instrument)而已,沒有特別標明是哪種樂器
這時候,你只需要把風笛當成一般樂器送去調音——調音師不會在意它原本是風笛還是鋼琴,他只會根據「樂器的方法」來進行操作。這個「把更專用的風笛,當作更通用的樂器來使用」的過程,就是 向上轉型(Upcasting)

class Instrument {
  public void play() {}
  static void tune(Instrument i) {
    // ...
    i.play();
  }
}

// Wind objects are instruments
// because they have the same interface:
public class Wind extends Instrument {
  public static void main(String[] args) {
    Wind flute = new Wind();
    Instrument.tune(flute); // Upcasting
  }
}

步驟

  1. 在 Java 中,Wind flute = new Wind(); 會先建立一個風笛物件
  2. 當我們呼叫 Instrument.tune(flute); 時,編譯器會自動把 Wind 型別的 flute 視為它的父類別 Instrument,完成向上轉型
  3. tune() 方法裡,只能呼叫 Instrument 類別上定義的方法(像 play()),因為編譯期只知道它是一個 Instrument

這樣做有幾種好處

    1. 統一介面:不用為每種樂器都寫不同的 tuneWind()tuneString() 方法,只要一個 tune(Instrument i) 就搞定所有
    2. 靈活擴充:以後新增鼓(Drum)、吉他(Guitar)等,只要繼承自 Instrument,都能直接傳給 tune()
注意:向上轉型後就不能呼叫子類別特有的方法了。比如如果 WindadjustReed(),在 tune(instrument) 中就看不到,因為編譯期認為它是個 Instrument

透過這個流程,我們把專用(風笛)的功能,放進通用(樂器)的模組裡,使程式設計更具彈性,也能同時兼顧擴充與維護

向上轉型的概念

  • 定義:將子類別型別的引用賦值給父類別型別的參考
Wind flute = new Wind();        // 建立 Wind 物件
Instrument inst = flute;        // 自動向上轉型,flute 被當作 Instrument
inst.play();                    // 呼叫父類別 play(),執行 Wind 中的實作
  • 安全性:向上轉型是安全的,因為任何子類別物件都「本來就是」父類別的一種,擁有父類別的所有方法和屬性,不會缺少功能
  • 限制條件:轉型後只能呼叫父類別定義的方法和屬性。例如若 Wind 類有 adjustReed(),執行:
inst.adjustReed(); // 編譯錯誤:Instrument 沒有這個方法

只能先向下轉型(Downcasting)Wind,才能調用子類別專屬方法

為何稱為向上轉型

在繼承結構圖中,父類別通常置於上方,子類別位於下方;將子類別標籤換成父類別標籤,就像從畫面中的下方向上方移動,所以叫 向上轉型(Upcasting)

  1. 子→父:把更特殊化的物件當作更一般化的類別來看待
  2. 視覺想像:就好像在一張家譜圖上,從子孫節點往祖先節點「向上走」

核心重點

  • 專用→通用:轉型後只剩父類別的方法與屬性可用,子類別獨有功能將被「隱藏」
  • 多型基石:Upcasting 是實現多型的關鍵,透過它可以用統一的父介面操作各種子物件
當你需要回到子類別的獨有功能,就要做「向下轉型(Downcasting)」,方向如同從家譜圖的上方滑回下方

向上轉型的意義

子類別的物件可以被當成父類別使用,藉此讓程式在執行時看不到子類別的特殊功能,而只專注於父類別的共同行為

  • 想像你有一支只能寫字的魔法筆(MagicPen),但你的同事只知道它是橡皮擦(Eraser)的一種——他只需要「擦除功能」。
  • 你把這支魔法筆當成普通橡皮擦交給他,他就能呼叫 erase() 功能。他不會知道這支筆額外能畫畫,但這不影響他的工作
  • 為什麼重要?
    • 多型基石:集合(如陣列或清單)存放各式子類別物件,例如 List<Eraser> list,只要它們都繼承自 Eraser,就能一起迭代呼叫 erase()
    • 統一介面:不必為每種子類別都寫不同的方法簽章,降低程式碼複雜度
    • 靈活擴充:以後新增 SmartEraserGelEraser,只要繼承自 Eraser,馬上能支援既有程式
  • 實際案例
    • 圖形繪製:定義 Shape 父類別,子類別有 CircleSquareTriangle。使用 drawAll(List<Shape> shapes),就能一次畫出所有形狀
    • 付款系統:定義 PaymentMethod,實作 CreditCardPaypalBitcoin。呼叫 process(PaymentMethod p),統一處理付款流程

這就是「把特殊當一般、再統一操作」的魔法,熟悉之後設計程式就能自然想到:需要多型時,就先考慮 Upcasting

小結:打造靈活又安全的物件導向程式

  1. 繼承 讓子類別自動擁有父類別共通行為,減少重複程式碼,修改時更集中化,讓程式結構更清晰易管理
  2. protected 提供「家族成員專屬通道」,隱藏內部實作又允許子類別安全存取,達成封裝與彈性的平衡
  3. 向上轉型(Upcasting) 將子類別視作父類別使用,是多型的基礎;透過統一介面,可以一鍵處理各種不同物件,極大提升擴充性與維護性
下次設計類別時,思考哪些行為可以抽到父類別,哪些成員需要對子類別開放,並評估是否用 Upcasting 統一操作流程

掌握這三大基礎,你的 Java 程式將更 乾淨靈活易維護,也更接近「物件導向大師」的境界