CodeGym /課程 /JAVA 25 SELF /方法覆寫 (overriding):與多載的差異

方法覆寫 (overriding):與多載的差異

JAVA 25 SELF
等級 18 , 課堂 2
開放

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()。子類別 DogCat 覆寫了此方法,提供各自的實作。

註解 @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。不能降低可見性(改為 protectedprivate)。

範例:

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(),而子類別 ManagerDeveloper 可以以各自的方式實作該方法:

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:嘗試覆寫 staticfinal 方法。 static 方法不會被覆寫,而 final 方法完全不能被覆寫。若嘗試這麼做——編譯器會阻止你。

錯誤 №4:將回傳型別改成不相容的型別。 如果子類別方法的回傳型別與超類別不一致(也不是其子型別),編譯器不允許覆寫。

錯誤 №5:新增未在基底方法宣告的受檢例外。 被覆寫的方法不得拋出基底方法未宣告過的新受檢例外。否則會出現編譯錯誤。

錯誤 №6:忘記 super 若你想在被覆寫的方法中保留父類別的一部分行為,請記得顯式呼叫 super.methodName()。Java 不會自動幫你做。

現在你已經了解覆寫方法如何運作、它與多載的差異,以及如何藉此在 Java 中實現多型。下一堂課我們將探討如何在實務中運用多型——配合集合、陣列與真實案例!

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION