National Chengchi University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

1

Local AI

--

Uedu Code

--

政治大學 AQI 58 25°C PM2.5 12
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 09

繼承

extends、super、方法覆寫與 Object 類別
Interaction實作 25 minute(s)

Learning Objectives

  • 用 extends 讓子類別繼承父類別的欄位與方法,再加上自己的新成員
  • 說明建立子類別物件時建構子的執行順序,在需要時用 super(…) 呼叫父類別的建構子
  • 覆寫(override)父類別的方法並加上 @Override,用 super.方法() 呼叫父類別原本的版本
  • 分辨 public、protected、private 對子類別的影響,說明 final 類別與 final 方法的作用
  • 覆寫 Object 的 toString 與 equals,並說明覆寫 equals 時為什麼也要覆寫 hashCode

為什麼需要繼承

第 08 課學會了用類別描述資料。寫一個系統時,常會遇到幾個類別「大部分一樣、只有一點不同」: 學生和老師都有姓名和信箱,學生多了學號,老師多了研究室;狗和貓都會吃東西,狗會叫、貓會抓老鼠。 把共同的部分複製到每個類別裡,之後要改就得每個地方都改。

繼承(inheritance)讓你把共同的部分寫在父類別(superclass),再讓子類別(subclass)用 extends 繼承它:

class Animal {             // 父類別:所有動物共同的部分
    String name;
    void eat() { … }
}

class Dog extends Animal { // 子類別:自動擁有 name 和 eat(),再加上自己的東西
    void bark() { … }
}

子類別和父類別是「是一種」(is-a)的關係:狗是一種動物,所以 Dog extends Animal 合理。 如果只是「有一個」(has-a),例如車子有引擎,應該把引擎當成車子的欄位,而不是讓 Car extends Engine。

子類別繼承欄位與方法

第 21 行建立的 Dog 物件有三個欄位:name、age 是從 Animal 繼承來的,breed 是 Dog 自己的。d.eat() 時框架名稱是 Animal.eat(),因為方法寫在 Animal 裡,但框架裡的 this 指向的是 Dog 物件。第 28 行的 Animal 物件沒有 breed:父類別不會擁有子類別新增的東西。

建構子與 super(…)

子類別不會繼承父類別的建構子,但建立子類別物件時,父類別的建構子一定會先執行,負責把「父類別的那部分」初始化好。順序是:

  1. 子類別建構子的第一行呼叫 super(…),進入父類別的建構子(父類別如果也有父類別,再往上一層)
  2. 最上層的建構子先執行完,再一層一層回到子類別,執行 super(…) 之後的程式
class Person {
    String name;
    Person(String name) { this.name = name; }
}

class Student extends Person {
    int grade;
    Student(String name, int grade) {
        super(name);          // 把 name 交給 Person 的建構子
        this.grade = grade;   // 再設定自己的欄位
    }
}
  • super(…) 必須寫在建構子的第一行(Java 21 的規定;JDK 25 起放寬,可以先寫不使用 this 的敘述,例如檢查參數)
  • 沒有寫 super(…) 時,編譯器會自動在第一行補上 super();,呼叫父類別不接受參數的建構子
  • 所以父類別如果沒有不接受參數的建構子(像上面的 Person 只有 Person(String name)),子類別的建構子就必須自己寫 super(…),否則編譯錯誤: constructor Person in class Person cannot be applied to given types
  • 同一行只能是 super(…) 或 this(…) 其中一個:this(…) 會先轉給同類別的另一個建構子,由那個建構子負責呼叫 super(…)

三層繼承的建構順序

注意左邊的框架:GradStudent 的建構子一開始就呼叫 super(…),接著 Student 也是,最後疊成三層,三個框架的 this 都指向同一個物件。真正第一個印出訊息的是最上層的 Person,然後才一層一層回到子類別。

想一想:繼承與建構子

1.

下面的程式會印出什麼?

class A { A() { System.out.print("A"); } }
class B extends A { B() { System.out.print("B"); } }
class C extends B { C() { System.out.print("C"); } }

public class Main {
    public static void main(String[] args) {
        new C();
        System.out.println();
    }
}
2.

下面的程式會印出什麼?

class P {
    P(String s) { }
}

class C extends P {
    C() { System.out.println("C"); }
}

public class Main {
    public static void main(String[] args) {
        new C();
    }
}
3.

下面的程式會印出什麼?

class A {
    A(int x) { System.out.print("A" + x); }
}

class B extends A {
    B() { super(7); System.out.print("B"); }
    B(int y) { this(); System.out.print(y); }
}

public class Main {
    public static void main(String[] args) {
        new B(9);
        System.out.println();
    }
}
第 1 次作答

覆寫與 @Override

繼承來的方法不一定適合子類別。子類別可以寫一個名稱、參數列都相同的方法,取代父類別的版本,這叫做覆寫(override):

class Shape {
    double area() { return 0; }
}

class Rect extends Shape {
    private double w, h;

    @Override              // 告訴編譯器:這個方法是要覆寫父類別的
    double area() { return w * h; }
}

覆寫和多載不同:多載(第 07 課)是同名、參數列不同的兩個方法並存;覆寫是子類別用相同參數列的方法取代父類別的版本。

覆寫的規則:

  • 名稱與參數列必須完全相同;回傳型別相同(或是原本回傳型別的子類別)
  • 存取權限不能變小:父類別是 public,子類別也必須是 public
  • private 方法不會被繼承,所以談不上覆寫;static 方法也不能被覆寫

@Override 是寫給編譯器看的標記(annotation)。加上之後,如果你把名稱拼錯(tostring)或參數寫錯, 其實並沒有覆寫到任何方法,編譯器會報錯 method does not override or implement a method from a supertype。 沒加 @Override 的話,拼錯的方法只會變成一個新方法,程式能編譯,卻默默地用了父類別的版本。所以覆寫時一律加上 @Override。

在子類別裡,super.方法() 可以呼叫父類別原本的版本,常用在「沿用父類別的做法,再多做一點」。

覆寫之後,即使是父類別寫的程式碼呼叫這個方法,執行的也是子類別的版本:Java 依照物件實際的類別決定要執行哪個方法。下一課的多型會深入介紹這件事。

覆寫 area 與 describe

Shape.describe() 裡呼叫了 area()。第 42 行 s.describe() 時 this 是 Shape,所以用 Shape.area();第 43 行 r.describe() 先進入 Rect.describe(),它用 super.describe() 呼叫父類別的版本,而父類別版本裡的 area() 呼叫到的卻是 Rect.area():看框架名稱就知道執行的是哪一個。

protected、final 與單一繼承

存取修飾字與子類別:

修飾字 子類別能不能直接使用
public 可以
protected 可以(同一個套件的類別也可以)
不寫 只有在同一個套件時可以
private 不行,連子類別也不能直接存取,要透過父類別提供的方法

子類別的物件裡還是有父類別的 private 欄位(動畫裡看得到),只是子類別的程式碼不能直接寫它的名稱, 寫了會編譯錯誤 … has private access in …。想讓子類別使用、又不想公開給所有人,就用 protected;但封裝的原則依然適用, 能用方法存取的,就不要開放欄位。

final:

  • final class:這個類別不能被繼承,例如 String 就是 final 類別,寫 class MyString extends String 會編譯錯誤 cannot inherit from final String
  • final 方法:子類別不能覆寫這個方法,用在「這個行為不允許被改變」的時候
  • final 變數(第 02 課):指定一次之後不能再改

單一繼承: Java 的類別只能 extends 一個父類別,class C extends A, B 是語法錯誤。這避免了兩個父類別有同名方法時不知道該用哪一個的問題。 需要「同時具備好幾種能力」時,Java 用介面(interface),一個類別可以實作多個介面(第 11 課)。

一個類別沒有寫 extends 時,它的父類別就是 java.lang.Object。所以每個類別最終都繼承自 Object,形成一棵以 Object 為根的繼承樹。

Object 類別:toString 與 equals

因為每個類別都繼承 Object,所有物件都有 Object 定義的幾個方法。最常覆寫的是:

方法 Object 原本的行為 通常覆寫成
public String toString() 回傳 類別名稱@雜湊碼,例如 Point@1b6d3586 回傳物件內容的文字說明
public boolean equals(Object o) 和 == 一樣,只比是不是同一個物件 比較欄位內容
public int hashCode() 依物件身分產生的整數,通常每個物件都不同 依照 equals 用到的欄位計算

toString() 會在字串相接("a 是 " + a)和 System.out.println(a) 時自動被呼叫。

覆寫 equals 的標準寫法:

@Override
public boolean equals(Object o) {
    if (this == o) return true;                   // 同一個物件
    if (!(o instanceof Point)) return false;      // 型別不對(o 是 null 時也是 false)
    Point p = (Point) o;                          // 轉型之後才能讀 p.x、p.y
    return x == p.x && y == p.y;
}

參數型別一定要寫 Object。寫成 equals(Point p) 的話,參數列和 Object.equals(Object) 不同,變成了多載而不是覆寫, HashSet、ArrayList.contains 這些只會呼叫 equals(Object) 的程式就用不到它。加上 @Override 時,編譯器會幫你抓出這個錯誤。

覆寫 toString 與 equals

第 28 行字串相接時,會自動進入 Point.toString()。第 29 行的 == 比參照,a、b 是兩個不同的物件;第 30 行呼叫覆寫過的 equals,逐步看它怎麼檢查型別、轉型、比較欄位。

覆寫 equals 時也要覆寫 hashCode

Java 有一條規則:兩個物件 equals 相等,hashCode() 就必須相同。 Object 原本的 hashCode 依物件身分計算,兩個分別建立的物件幾乎一定不同;所以只覆寫 equals、不覆寫 hashCode,就違反了這條規則。

違反的後果出現在 HashSet、HashMap 這類用雜湊找資料的集合(第 13 課)。它們加入或尋找元素時:

  1. 先用 hashCode() 算出要去哪一格找
  2. 那一格裡有雜湊值相同的元素,才用 equals 確認是不是真的相同

hashCode 不同,HashSet 會直接認定是不同的元素,連 equals 都不呼叫。結果是同樣內容的牌被放進去兩次,contains 也找不到。

覆寫 hashCode 最簡單的做法是用 java.util.Objects.hash(…),把 equals 比較的同一組欄位傳進去:

@Override
public int hashCode() {
    return Objects.hash(suit, rank);
}

反過來,hashCode 相同不一定 equals 相等(不同內容也可能剛好算出同一個數字),這是允許的,只是會讓集合多做幾次 equals 比較。

沒有覆寫 hashCode 的後果

Card 只覆寫了 equals,GoodCard 繼承 Card 並補上 hashCode。前半段把兩張一樣的 Card 放進 HashSet,注意動畫裡完全沒有呼叫 equals,兩張都被放進去;後半段每次 add 都先進入 GoodCard.hashCode(),雜湊值相同時才進入 equals。

想一想:覆寫與 Object

1.

下面的程式會印出什麼?

class A {
    String hi() { return "A"; }
    String call() { return hi(); }
}

class B extends A {
    @Override
    String hi() { return "B"; }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(new B().call() + new A().call());
    }
}
2.

下面的程式會印出什麼?(注意方法名稱的大小寫)

class A {
    public String toString() { return "A"; }
}

class B extends A {
    @Override
    public String tostring() { return "B"; }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(new B());
    }
}
3.

P 沒有覆寫任何 Object 的方法。下面的程式會印出什麼?

class P {
    int x = 1;
}

public class Main {
    public static void main(String[] args) {
        P p = new P();
        String s = p.toString();
        System.out.println(s.startsWith("P@") + " " + p.equals(new P()) + " " + p.equals(p));
    }
}
4.

下面的程式能不能編譯?

final class Base { }

class Derived extends Base { }

public class Main {
    public static void main(String[] args) {
        System.out.println("ok");
    }
}
第 1 次作答

動手寫:主管的薪資

Employee 已經寫好。請完成 Manager:建構子用 super(…) 把姓名與底薪交給父類別,再設定自己的 bonus;覆寫 pay(),回傳父類別算出的底薪加上獎金。toString() 寫在 Employee 裡,會呼叫 pay()。

你的輸出
預期輸出

重點回顧

  • class 子類別 extends 父類別:子類別擁有父類別的欄位與方法,再加上自己的;兩者是「是一種」的關係
  • 建立子類別物件時,父類別的建構子先執行;super(…) 寫在建構子第一行,沒寫時自動補 super();,父類別沒有無參數建構子時必須自己寫
  • 覆寫:子類別用相同名稱與參數列取代父類別的方法,權限不能變小;一律加 @Override 讓編譯器檢查;super.方法() 呼叫父類別原本的版本
  • 執行哪個版本的方法,由物件實際的類別決定,即使呼叫寫在父類別裡
  • private 成員子類別不能直接存取,protected 可以;final 類別不能被繼承,final 方法不能被覆寫
  • Java 的類別只能繼承一個父類別;沒寫 extends 時父類別是 Object
  • 覆寫 toString 讓物件好印;覆寫 equals 時參數型別是 Object,而且一定要一起覆寫 hashCode(用 Objects.hash 傳入同一組欄位),否則 HashSet、HashMap 會出錯
這一課完成了!
Next lesson:多型
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。