CodeGym /課程 /JAVA 25 SELF /繼承的問題與限制

繼承的問題與限制

JAVA 25 SELF
等級 17 , 課堂 4
開放

1. Java 中繼承的限制

僅支援類別的單一繼承。 在 Java 中,一個類別只能繼承自另一個類別。這稱為 單一繼承。例如,這樣是可以的:

class Animal { }
class Dog extends Animal { }

但這樣就不行:

class Animal { }
class Robot { }
// 錯誤! Java 不支援多重類別繼承
class RoboDog extends Animal, Robot { }

若嘗試宣告這樣的類別,編譯器會提示:"class RoboDog cannot extend multiple classes"。為什麼?因為多重繼承會造成歧義:如果兩個父類別都有相同簽章的方法,應該使用哪一個?這就是著名的「鑽石問題」(diamond problem)。

介面 在 Java 中可以實作任意多個,但我們尚未講到;之後再談。

建構子不會被繼承。 即便父類別有好用的建構子,子類別也不會自動擁有。必須在子類別的建構子中透過 super(...) 明確呼叫父類別的建構子。

private 成員不會被繼承。 父類別所有的 private 欄位與方法在子類別中不可見。它們存在於物件「內部」,但無法直接存取。

2. 脆弱階層的問題

類別之間的強耦合。 當你建立繼承階層時,子類別會與父類別緊密耦合。若你修改了基底類別,可能會影響(甚至破壞)它的所有子類別。想像你有一個 Animal 類別,DogCatBird 等十多個類別都繼承自它。若你改動 Animal 的結構(例如在建構子加入新的必填參數),就得把所有子類別都走一遍並更新其程式碼。在大型專案中特別痛苦。

會破壞基底行為的繼承問題。 有時子類別可能不小心改變了基底類別所仰賴的行為。比如,父類別在某個方法中呼叫了自己的另一個方法,而子類別覆寫了該方法並改變了其邏輯。結果父類別開始出現非預期的行為。

class Animal {
    void makeSound() {
        System.out.println("Some sound");
    }
    void sleep() {
        System.out.println("Animal is going to sleep...");
        makeSound(); // 父類別在呼叫自己的方法
    }
}

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

public class Main {
    public static void main(String[] args) {
        Animal a = new Dog();
        a.sleep();
    }
}

程式會輸出什麼?

Animal is going to sleep...
Woof!

父類別原本以為 makeSound() 會執行自己的實作,但實際上呼叫到的是子類別的版本!若子類別以不同邏輯覆寫方法,就可能導致難以預料的錯誤。

3. 脆弱基底類別問題(fragile base class problem)

在大型專案中這是真實存在的問題。當你修改基底類別(例如新增欄位、變更方法實作)時,就有可能破壞所有子類別的行為。有時不會立刻顯現,找出這類錯誤可能要花上數小時甚至數天。

示意:假設你有一個 Shape 類別,具有 draw() 方法。你決定在 Shape 新增 drawShadow(),而它會呼叫 draw()。但其中一個子類別(Circle)覆寫了 draw(),於是在 Circle 上呼叫 drawShadow() 時,行為可能變得出乎意料。

4. 強耦合與重構困難

當類別透過繼承彼此耦合時,修改其中一個類別可能影響整條相依鏈。這會讓程式碼的彈性變差,並使重構與擴充變得困難。有時為了加入新功能,甚至必須重寫整個階層。

實務範例

class Vehicle { /* ... */ }
class Car extends Vehicle { /* ... */ }
class Bicycle extends Vehicle { /* ... */ }
class Bus extends Vehicle { /* ... */ }

忽然來了新需求:「那我們加上電動滑板車吧!」。但電動滑板車既是交通工具,又像是小裝置。該怎麼辦?如果你開始擴充階層以納入所有新實體,它很快就會變得難以掌控。

5. 沒有語意關係卻為了重用而繼承的問題

很多初學者(不只初學者)會為了重用程式碼而使用繼承,即便類別之間並不存在「is-a」關係。這會導致錯誤的架構。

錯誤繼承的範例

class DatabaseUtils {
    void connect() { /* ... */ }
    void disconnect() { /* ... */ }
}

class User extends DatabaseUtils { // User 並不是資料庫工具(不是 is-a 關係)!
    String name;
}

更正確的做法是使用組合:讓 DatabaseUtils 成為獨立的類別,並在需要的地方呼叫其方法,而不是從它繼承。

6. 繼承的替代方案

組合(has-a

當一個物件「包含」另一個物件時,請使用組合。例如,Car 類別可以有一個 Engine 欄位:

class Engine { /* ... */ }

class Car {
    private Engine engine;
    // ...
}

委派

與其擴充某個類別,不如把任務委派給另一個物件。這能保有彈性並降低元件間的耦合。

介面

在 Java 中,類別可以實作任意多個介面。這讓你能在不建立僵硬階層的前提下,彈性地組合各種行為。介面我們之後再回來談。

什麼時候該使用繼承?

只有在類別之間存在明確的「is-a」關係時才使用繼承:

  • 貓是動物(Cat extends Animal
  • 圓形是圖形(Circle extends Shape
  • 管理員是使用者(Admin extends User

不要只為了重用程式碼而使用繼承——為此有組合與委派可用。

7. 幾個實務範例

範例:過度複雜的階層

class Animal { }
class Mammal extends Animal { }
class Cat extends Mammal { }
class PersianCat extends Cat { }
class SuperPersianCat extends PersianCat { }

如果你的階層超過三層,就該思考是否該停下來了。過深的階層會讓理解與維護變得困難。

範例:扁平的階層

class Animal { }
class Cat extends Animal { }
class Dog extends Animal { }
class Bird extends Animal { }
class Fish extends Animal { }
class Spider extends Animal { }
class Platypus extends Animal { }
class Dragon extends Animal { }

若你有成打的子類別,而且彼此只在單一方法上不同,可能更適合使用介面或組合。

8. 使用繼承時的常見錯誤

錯誤 1:沒有「is-a」關係卻使用繼承。
如果子類別其實不是父類別的一種,架構就會不自然且很快失控。比如,User 不該繼承自 DatabaseUtils,就算這看起來「很方便」。

錯誤 2:覆寫方法卻改變其契約。
若你在覆寫方法時改變了其邏輯,導致不再符合父類別的預期,就會引發意外錯誤。比如,基底類別預期 draw() 會繪製圖形,而在子類別中它卻開始執行危險的副作用——這是災難。

錯誤 3:階層過深或過於扁平。
過深的階層讓程式難以理解;過於扁平則造成重複。

錯誤 4:試圖繞過語言限制。
試圖用各種「權宜之計」來實現多重繼承(大量複製貼上、「工具型」超類別),最終只會造成混亂。

錯誤 5:盲目用繼承來重用程式碼。
往往導致類別之間出現意料之外的耦合,讓測試與維護更加困難。請使用組合與委派。

1
問卷/小測驗
繼承與階層,等級 17,課堂 4
未開放
繼承與階層
繼承與階層
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION