1. 繼承中的錯誤
繼承是 OOP 的基石之一,但也是新手最常踩雷的主題之一。我們來看看經典錯誤,並學會如何避免。
沒有呼叫基底類別的建構式 (super(...))
當你建立子類別時要記得:基底類別可能要求透過建構式進行特定的初始化。若基底類別沒有預設(無參數)建構式,則在子類別的建構式中一定要用 super(...) 明確呼叫基底類別的建構式。
錯誤範例:
class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
}
class Dog extends Animal {
// 錯誤!Animal 沒有無參數(預設)建構式
public Dog() {
// super(); // 編譯器會自動插入 super(),但基底類別並沒有這樣的建構式!
}
}
如何修正:
class Dog extends Animal {
public Dog(String name) {
super(name); // 這樣就沒問題!
}
}
說明:
如果基底類別只有帶參數的建構式,編譯器不會自動產生無參數建構式。這常是編譯錯誤的原因。
試圖繼承自 final 類別或覆寫 final 方法
在 Java 中可以將類別或方法宣告為 final。這表示:
- 類別不可被繼承。
- 方法不可在子類別中覆寫(override)。
錯誤範例:
final class Cat {}
// 編譯錯誤!
class Tiger extends Cat {
// ...
}
class Animal {
public final void sleep() {
System.out.println("Zzz...");
}
}
class Dog extends Animal {
// 編譯錯誤!
@Override
public void sleep() {
System.out.println("Dog is sleeping...");
}
}
說明:
如果你看到「cannot inherit from final」、「cannot override final method」等錯誤,請檢查修飾詞!
違反里氏替換原則(Liskov Substitution Principle)
聽起來很可怕,但實際上意思是:子類別的物件必須能像基底類別的物件一樣行為,而不破壞程式邏輯。常見錯誤是把方法覆寫成與基底預期不一致的行為。
範例:
class Bird {
public void fly() {
System.out.println("我在飛!");
}
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("企鵝不會飛!");
}
}
問題在哪裡?
處理 Bird 的程式碼會假設任何鳥都會飛;但若傳入 Penguin,程式可能會在執行時失敗。
較佳作法:
此時應重新檢視階層設計,或改用介面/組合。
2. 方法多載(overloading)的常見錯誤
多載是同一個類別中有多個同名但參數不同的方法。看似簡單,卻也容易踩坑。
把覆寫誤寫成多載(簽章錯誤)
新手常想覆寫(override)基底方法,但不小心改了參數。結果變成多載,而不是覆寫——多型不起作用!
錯誤範例:
class Animal {
public void makeSound() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
// 原本想覆寫,卻變成多載!
public void makeSound(String extra) {
System.out.println("Bark! " + extra);
}
}
問題:
呼叫 dog.makeSound() 會執行父類別的方法,而不是你新增的那個。
呼叫 dog.makeSound("loudly") 會走到多載的方法,但多型不起作用!
最佳實務:
請使用註解 @Override——若你在簽章上出錯,編譯器會立刻提醒。
@Override
public void makeSound() { /* ... */ }
多載的非直觀行為(自動型別轉換、呼叫歧義)
當參數型別同時符合多個多載版本時,Java 有時會選擇與你預期不同的方法。
public class OverloadDemo {
public void print(int x) {
System.out.println("int: " + x);
}
public void print(double x) {
System.out.println("double: " + x);
}
}
OverloadDemo demo = new OverloadDemo();
demo.print(5); // int: 5
demo.print(5.0); // double: 5.0
demo.print(5L); // long -> double: double: 5.0
問題:
如果呼叫 demo.print(5L),Java 會選擇 print(double x)(因為 long 轉成 double 比轉成 int 更合適)。
若同時存在參數型別為 Object、Integer、int 的多載,使用 null 呼叫可能導致編譯錯誤:「reference to print is ambiguous」。
僅以回傳型別做區分的同名方法(編譯錯誤)
在 Java 中不能宣告兩個只有回傳型別不同、參數相同的同名方法!
public class Demo {
// 編譯錯誤!
public int foo() { return 1; }
public String foo() { return "hello"; }
}
說明:
用於多載判斷的方法簽章是名稱 + 參數,回傳型別不被考慮。編譯器無法判斷你要呼叫哪一個方法。
3. 最佳實務
為了避免在繼承與多載上踩雷,請遵循以下建議:
覆寫方法時務必使用 @Override 註解
這不僅提高可讀性,也能防止簽章錯誤;一旦你改了參數或方法名稱,編譯器會立刻告知。
@Override
public void makeSound() {
System.out.println("Bark!");
}
清楚區分多載與覆寫
- 覆寫(override):改變父類別方法的行為——簽章必須完全相同。
- 多載(overload):新增同名方法,但參數不同。
對照表:
| 多載(overloading) | 覆寫(overriding) | |
|---|---|---|
| 位置 | 同一類別/繼承層級內 | 在子類別中 |
| 方法名稱 | 相同 | 相同 |
| 參數 | 不同 | 相同 |
| 回傳型別 | 可以不同 | 必須相同/相容 |
| 註解 | 非必須 | @Override 建議使用 |
避免過度使用多載
如果某個方法有太多多載版本,程式會變得難讀且混亂。當選項過多時,最好使用參數物件或 Builder 模式。
4. 範例:寵物管理系統
假設你要做一個簡單的寵物管理系統。你有基底類別 Pet 與子類別 Cat 和 Dog。
public class Pet {
private String name;
public Pet(String name) {
this.name = name;
}
public void speak() {
System.out.println(name + " 發出不明的聲音。");
}
}
public class Cat extends Pet {
public Cat(String name) {
super(name);
}
@Override
public void speak() {
System.out.println(getName() + " 說:喵!");
}
// 錯誤:Pet 中沒有 getName() 方法!
}
典型錯誤:
在嘗試覆寫方法時,卻呼叫了基底類別沒有的方法。應該新增一個 getter:
public class Pet {
private String name;
public Pet(String name) { this.name = name; }
public String getName() { return name; }
public void speak() { System.out.println(name + " 發出不明的聲音。"); }
}
現在一切正常,可以使用多型:
Pet myPet = new Cat("Barsik");
myPet.speak(); // Barsik 說:喵!
5. 繼承與多載的常見錯誤彙總
錯誤一:忘了呼叫 super(...)。
如果基底類別的建構式含有重要邏輯而你沒有呼叫,程式可能行為異常,甚至無法編譯。
錯誤二:覆寫了錯的方法。
原本想改變父類別方法的行為,卻新增了一個參數不同或名字相近的新方法。結果:舊方法照樣執行,而你的新方法沒有人呼叫。
錯誤三:嘗試覆寫 final 方法。
Java 不允許這麼做,這是好事!但如果看到編譯錯誤——請檢查是否有 final。
錯誤四:把方法多載得讓人看不懂。
當你有 10 個 calculate 的多載版本,而且你自己也搞混時——是時候停下來考慮重構了。
錯誤五:違反里氏替換原則。
如果子類別改變了基底類別的行為語意,整個架構都可能歪掉。比如有個 Shape 類別有 getArea(),而子類別 BrokenShape 回傳 -1,就可能造成奇怪的 bug。
GO TO FULL VERSION