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 類別,Dog、Cat、Bird 等十多個類別都繼承自它。若你改動 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:盲目用繼承來重用程式碼。
往往導致類別之間出現意料之外的耦合,讓測試與維護更加困難。請使用組合與委派。
GO TO FULL VERSION