1. 覆寫方法
覆寫(overriding)是指在子類別中撰寫父類別已存在方法的自訂版本。藉此,程式在執行期(run-time)才能實現真正的多型。
白話來說:
如果你有一個基底類別帶有某個方法,而你希望子類別以自己的方式來執行它,只要在子類別宣告一個相同簽名的方法即可。當透過基底型別的參考呼叫該方法時,會呼叫物件實際型別中的方法版本。
生活中的例子。
想像你有一隊動物,你要每個成員「發出聲音」。對所有動物來說,指令都是 makeSound(),但狗會汪叫,貓會喵叫,牛會哞叫。對程式碼來說,看起來都是呼叫同一個方法,但結果各不相同——這就是覆寫的魔力!
覆寫的語法
基本範例
class Animal {
void makeSound() {
System.out.println("動物發出某種聲音");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("汪!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("喵!");
}
}
這裡在類別 Animal 中定義了方法 makeSound()。子類別 Dog 與 Cat 覆寫了此方法,提供各自的實作。
註解 @Override
在 Java 中建議(也是個好習慣)用註解 @Override 標記被覆寫的方法:
@Override
void makeSound() { ... }
這對程式運作不是必需的,但:
- 編譯器會檢查你是否真的在覆寫方法(而不是因為方法名或參數打錯而意外定義了新方法)。
- 提升程式碼可讀性——其他開發者一眼就能看出這個方法覆寫了父類別的方法。
呼叫被覆寫的方法
Animal myDog = new Dog();
myDog.makeSound(); // 會輸出:汪!
雖然變數型別是 Animal,但實際上它指向的是 Dog 物件,因而呼叫的是 Dog 類別中的方法。這就是動態(晚期)繫結。
2. 覆寫(overriding)與多載(overloading)的差異
多載(overloading)
- 在同一個類別(或在繼承層級中,但仍是彼此「相鄰」)。
- 方法名稱相同,但參數不同(型別、數量、順序)。
- 方法的選擇發生在編譯期。
void print(int x) { ... }
void print(String s) { ... }
覆寫(overriding)
- 在不同的類別中:方法在超類別宣告,並在子類別中覆寫。
- 方法具有相同的名稱與簽名(參數與回傳型別)。
- 方法的選擇發生在執行期(run-time),依據物件的實際型別決定。
比較表
| 多載(overloading) | 覆寫(overriding) | |
|---|---|---|
| 在哪裡 | 在同一個類別中 | 在超類別與子類別 |
| 方法名稱 | 相同 | 相同 |
| 參數 | 不同 | 相同 |
| 回傳型別 | 可以不同 | 必須相同或為其子型別 |
| 選擇時機 | 編譯期 | 執行期(run-time) |
| 註解 | 不需要 | @Override(建議使用) |
3. 覆寫方法的規則
覆寫很強大,但伴隨嚴格的規則。讓我們逐一說明。
方法簽名必須一致
- 方法名稱、參數型別與順序必須與超類別中的方法完全相同。
- 回傳型別必須相同,或是協變(也就是父方法回傳型別的子型別)。
協變回傳型別範例:
class Animal {
Animal reproduce() { return new Animal(); }
}
class Cat extends Animal {
@Override
Cat reproduce() { return new Cat(); } // OK!Cat 是 Animal 的子型別
}
存取修飾子
被覆寫方法的存取修飾子不可比超類別方法更嚴格。 如果父類別的方法是 public,那子類別中也必須維持為 public。不能降低可見性(改為 protected 或 private)。
範例:
class Parent {
public void greet() { }
}
class Child extends Parent {
// void greet() { } // 錯誤!預設(package-private)比 public 更不開放
@Override
public void greet() { } // OK
}
例外
- 被覆寫的方法不能拋出父方法未宣告過的新受檢例外。
- 可以拋出更少或相同的例外。
範例:
class Parent {
void doWork() throws IOException { }
}
class Child extends Parent {
@Override
void doWork() throws FileNotFoundException { } // OK,FileNotFoundException 是 IOException 的子型別
// void doWork() throws SQLException { } // 錯誤!父類別未宣告 SQLException
}
static 方法不會被覆寫
static 方法可以被「隱藏」(hidden),但不會被覆寫。 如果你在子類別宣告了相同簽名的 static 方法,這不是覆寫!這只是方法隱藏,並非多型。
class Animal {
static void info() { System.out.println("Animal"); }
}
class Dog extends Animal {
static void info() { System.out.println("Dog"); }
}
呼叫 Dog.info() 會輸出 "Dog",但若透過 Animal 型別的變數呼叫,則會執行 Animal.info()。這不是多型!
final 方法不可覆寫
如果超類別的方法被宣告為 final,試圖覆寫它會導致編譯錯誤。
class Animal {
final void sleep() { }
}
class Dog extends Animal {
// @Override
// void sleep() { } // 錯誤!不可覆寫 final 方法
}
4. 實用範例
讓我們實際看看覆寫如何運作,以及它與多載有何差異。
範例 1:Shape 類別及其子類
class Shape {
void draw() {
System.out.println("繪製圖形");
}
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("繪製圓形");
}
}
class Rectangle extends Shape {
@Override
void draw() {
System.out.println("繪製長方形");
}
}
使用多型:
public class Main {
public static void main(String[] args) {
Shape s1 = new Circle();
Shape s2 = new Rectangle();
s1.draw(); // 繪製圓形
s2.draw(); // 繪製長方形
}
}
雖然變數宣告為 Shape,但實際呼叫的是物件所屬類別中的方法。
範例 2:與多載的差異
class Printer {
void print(String s) {
System.out.println("字串:" + s);
}
void print(int n) {
System.out.println("數字:" + n);
}
}
這裡兩個方法都叫做 print,但參數不同——這是多載,而不是覆寫。
5. 覆寫與呼叫父類別方法(super)
有時在被覆寫的方法中,你會希望先執行父類別的邏輯,再加入自己的邏輯。這時可使用關鍵字 super。
class Animal {
void makeSound() {
System.out.println("動物發出聲音");
}
}
class Dog extends Animal {
@Override
void makeSound() {
super.makeSound(); // 呼叫父類別的方法
System.out.println("汪!");
}
}
呼叫 new Dog().makeSound() 會輸出:
動物發出聲音
汪!
動態繫結(late binding)如何運作
當你透過基底型別的參考呼叫方法時,Java 會在執行期判斷這個參考實際指向的物件,並呼叫該物件所屬類別中定義的方法版本。
Animal a = new Cat();
a.makeSound(); // 會呼叫 Cat.makeSound(),而不是 Animal.makeSound()
這正是 Java 多型的基礎。
6. 與你的應用程式有何關聯
在我們的教學應用(例如員工管理系統)中,你可以建立基底類別 Employee 與方法 work(),而子類別 Manager 與 Developer 可以以各自的方式實作該方法:
class Employee {
void work() {
System.out.println("員工在工作");
}
}
class Manager extends Employee {
@Override
void work() {
System.out.println("經理在管理");
}
}
class Developer extends Employee {
@Override
void work() {
System.out.println("開發者在寫程式碼");
}
}
現在你可以把所有員工保存在同一個陣列或清單中:
Employee[] employees = {new Manager(), new Developer(), new Developer()};
for (Employee e : employees) {
e.work(); // 每個都會輸出各自的內容!
}
7. 覆寫方法時的常見錯誤
錯誤 №1:方法名稱或參數拼錯。 如果你不小心把方法名稱或參數寫錯,你不是在覆寫,而是在建立一個新方法,結果多型不會生效。這就是為什麼總是應該使用 @Override 註解——編譯器會立即提醒你。
錯誤 №2:使用更嚴的存取修飾子。 如果父類別的方法是 public,你在子類別把它宣告為 protected 或不加修飾子,會得到編譯錯誤。
錯誤 №3:嘗試覆寫 static 或 final 方法。 static 方法不會被覆寫,而 final 方法完全不能被覆寫。若嘗試這麼做——編譯器會阻止你。
錯誤 №4:將回傳型別改成不相容的型別。 如果子類別方法的回傳型別與超類別不一致(也不是其子型別),編譯器不允許覆寫。
錯誤 №5:新增未在基底方法宣告的受檢例外。 被覆寫的方法不得拋出基底方法未宣告過的新受檢例外。否則會出現編譯錯誤。
錯誤 №6:忘記 super。 若你想在被覆寫的方法中保留父類別的一部分行為,請記得顯式呼叫 super.methodName()。Java 不會自動幫你做。
現在你已經了解覆寫方法如何運作、它與多載的差異,以及如何藉此在 Java 中實現多型。下一堂課我們將探討如何在實務中運用多型——配合集合、陣列與真實案例!
GO TO FULL VERSION