National Yang Ming Chiao Tung 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 69 24°C PM2.5 26
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 11

介面與抽象類別

interface、abstract、default 方法與 record
Interaction實作 25 minute(s)

Learning Objectives

  • 用 interface 定義「能做什麼」,用 implements 讓類別實作,並記得實作的方法要寫 public
  • 讓一個類別實作多個介面,用介面型別的變數指向不同類別的物件
  • 說明 default 方法、static 方法與介面常數的用途
  • 分辨抽象類別與介面,依情況選用
  • 實作 Comparable 讓自己的物件可以排序,並用 record 簡潔地寫出只裝資料的類別

為什麼需要介面

第 10 課用父類別 Animal 統一處理狗和貓。可是有些共通點不適合用繼承表達:鴨子、魚、游泳選手都「會游泳」, 它們卻不是同一種東西,也不可能有共同的父類別。而且 Java 的類別只能繼承一個父類別。

介面(interface)描述的是「能做什麼」,不管你是什麼:

interface Swimmer {
    void swim();          // 只說「要有 swim()」,不寫怎麼做
}

class Duck implements Swimmer {          // Duck 承諾:我會 swim()
    public void swim() { System.out.println("鴨子划水"); }
}
  • interface 裡的方法只有標頭、沒有本體,預設就是 public abstract(公開、抽象),不必寫出來
  • 類別用 implements 實作介面,必須寫出介面裡每一個抽象方法,否則無法編譯
  • 實作的方法一定要加 public。介面方法是 public,覆寫時存取權限不能變小,忘了寫會編譯錯誤:attempting to assign weaker access privileges; was public

介面就像一份合約:只要簽了(implements),別人就可以放心呼叫合約上的方法,不必知道你是哪個類別。

一個類別實作多個介面

Duck 同時實作 Swimmer 和 Flyer,所以同一隻鴨子可以放進 Swimmer[],也可以交給 Flyer 型別的變數。race 方法只認得 Swimmer,它不知道也不在乎陣列裡是鴨子還是魚。

default 方法、static 方法與常數

早期的介面只能有抽象方法。Java 8 起,介面還可以有兩種有本體的方法:

interface Greeter {
    String PREFIX = "【Uedu】";          // 常數

    String name();                       // 抽象方法

    default String greet() {             // default 方法
        return PREFIX + "你好,我是 " + name();
    }

    static String line(int n) {          // static 方法
        return "-".repeat(n);
    }
}
成員 寫法 說明
抽象方法 String name(); 預設 public abstract,實作類別一定要寫
default 方法 default String greet() { … } 有預設的實作;實作類別可以直接用,也可以覆寫
static 方法 static String line(int n) { … } 屬於介面本身,用 Greeter.line(5) 呼叫;實作類別不會繼承它
常數 String PREFIX = "…"; 介面的欄位一律是 public static final,宣告時就要給值,之後不能修改

default 方法讓函式庫可以替已經存在的介面加上新方法,而不會讓所有實作它的舊類別無法編譯(舊類別直接沿用預設的實作)。 注意介面沒有一般的欄位:它不能記住每個物件各自的資料,所以 greet() 要取得名字,只能呼叫抽象方法 name(),由實作類別提供。

用預設的實作,或覆寫它

Student 沒有寫 greet(),所以第 34 行進入的是介面的 default 方法 Greeter.greet(),它再呼叫 Student 寫的 name()。Robot 覆寫了 greet(),第 35 行就進入 Robot.greet()。

想一想:介面的規則

1.

下面的程式能不能編譯?會印出什麼?

interface Speaker {
    void speak();
}

class Dog implements Speaker {
    void speak() { System.out.println("汪"); }
}

public class Main {
    public static void main(String[] args) {
        Speaker s = new Dog();
        s.speak();
    }
}
2.

下面的程式會印出什麼?

interface Shape {
    double area();
    default String describe() { return "面積 " + area(); }
}

class Sq implements Shape {
    public double area() { return 4; }
}

class Big implements Shape {
    public double area() { return 100; }
    public String describe() { return "很大"; }
}

public class Main {
    public static void main(String[] args) {
        Shape a = new Sq();
        Shape b = new Big();
        System.out.println(a.describe() + "/" + b.describe());
    }
}
3.

下面的程式能不能編譯?

interface Config {
    int MAX = 10;
}

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

抽象類別

第 10 課的 Shape 讓 area() 回傳 0,其實很勉強:「一個形狀」的面積根本沒有意義。更好的寫法是把它宣告成抽象:

abstract class Shape {
    String name;                       // 抽象類別可以有欄位
    Shape(String name) { … }           // 也可以有建構子
    abstract double area();            // 抽象方法:沒有本體,以分號結尾
    void show() { … area() … }         // 也可以有寫好的方法
}
  • 有抽象方法的類別一定要宣告成 abstract
  • 抽象類別不能直接 new(Shape is abstract; cannot be instantiated),只能 new 它的子類別
  • 子類別必須覆寫所有抽象方法;沒有全部覆寫的話,子類別也必須宣告成 abstract
  • 抽象類別仍然是類別:可以有欄位、建構子、private 方法,子類別用 extends 繼承,而且只能繼承一個

抽象類別留下的空格

show() 是 Shape 寫好的方法,所有子類別共用;它在第 12 行呼叫的 area() 是抽象方法,執行時依物件分別進入 Square.area() 與 Triangle.area()。

抽象類別還是介面?

抽象類別(abstract class) 介面(interface)
表達的關係 「是一種」:Square 是一種 Shape 「能做到」:Duck 能 Swim
一個類別可以有幾個 只能 extends 一個 可以 implements 很多個
欄位 可以有一般欄位,每個物件各一份 只有 public static final 常數
建構子 可以有 沒有
方法 抽象方法、一般方法都可以,存取修飾字不限 抽象方法、default 方法、static 方法
何時選用 幾個類別關係密切,要共用欄位與程式碼 不相關的類別要提供同一種能力;想讓類別保有繼承別人的自由

一般的建議是:先想介面,因為它不會占用類別唯一的繼承機會;當你發現好幾個實作需要共用欄位與程式碼時,再加一個抽象類別。 兩者也可以一起用,例如 abstract class Animal implements Comparable<Animal>。

函式介面:只有一個抽象方法的介面(例如 Comparator、Runnable)叫做函式介面,可以用 Lambda 表示式簡潔地寫出實作,第 17 課會詳細介紹。

Comparable:讓物件可以排序

Arrays.sort 可以排整數、字串,因為 Java 知道數字和字串要怎麼比大小。你自己寫的 Student,Java 不知道要比姓名還是分數。 做法是實作 Java 內建的 Comparable<T> 介面,它只有一個方法:

public int compareTo(T other);

回傳值的意義:負數表示 this 排在 other 前面、0 表示一樣、正數表示 this 排在後面。 比較整數時用 Integer.compare(a, b),它會依 a 小於、等於、大於 b 回傳 -1、0、1。 (也常看到 return this.score - other.score;,數值很大時相減可能溢位,Integer.compare 比較安全。)

Comparable<Student> 角括號裡的型別表示「和誰比」,這是泛型的寫法,第 13、14 課會再說明。 String、Integer 都已經實作 Comparable,所以可以直接排序。

依分數排序學生

第 29 行直接呼叫 compareTo,看它回傳什麼。第 30 行的 Arrays.sort 在排序過程中會自己呼叫 compareTo 比較兩個學生,你只負責定義「誰排前面」。排序之後,右邊陣列格子裡的箭頭順序會改變。

record:只裝資料的類別

很多類別只是用來裝幾個資料,卻要寫一大堆:private final 欄位、建構子、getter、toString、equals。 Java 16 起正式提供 record,一行就宣告完成:

record Book(String title, int price) { }

編譯器會自動產生:

  • 兩個 private final 欄位 title、price(建立之後不能修改)
  • 依參數順序的建構子 new Book("Java 入門", 450)
  • 存取方法 title()、price()(注意不是 getTitle())
  • toString()(印出 Book[title=Java 入門, price=450])、依內容比較的 equals() 與 hashCode()

record 可以實作介面、可以加方法,也可以寫精簡建構子(compact constructor):只寫 Book { … },不用列參數,用來檢查資料。 record 不能 extends 其他類別,也不能再加一般的實例欄位。適合座標、成績紀錄、商品資料這類「建立之後就不再改變」的資料。

實作介面的 record

Book 只有一行宣告,加上一個檢查價格的精簡建構子。注意第 22 行直接印出物件、第 23 行用 title() 讀資料,第 24 行用的是介面 Priced 的 default 方法:record 實作的 price() 剛好就是介面要求的抽象方法。

想一想:抽象類別、Comparable 與 record

1.

下面的程式會怎樣?

abstract class Shape {
    abstract double area();
}

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

下面的程式會印出什麼?

import java.util.Arrays;

class Item implements Comparable<Item> {
    String name;
    int price;
    Item(String name, int price) { this.name = name; this.price = price; }

    public int compareTo(Item o) {
        return Integer.compare(o.price, this.price);
    }
}

public class Main {
    public static void main(String[] args) {
        Item[] items = { new Item("筆", 30), new Item("書", 300), new Item("尺", 50) };
        Arrays.sort(items);
        for (Item it : items) System.out.print(it.name);
        System.out.println();
    }
}
3.

下面的程式會印出什麼?

record Point(int x, int y) { }

public class Main {
    public static void main(String[] args) {
        Point p = new Point(1, 2);
        Point q = new Point(1, 2);
        System.out.println(p + " " + p.equals(q));
    }
}
4.

Bird、Plane、Superman 彼此沒有關係,但都要提供 fly(),讓同一段程式可以讓它們一起起飛。最適合的設計是哪一個?

第 1 次作答

動手寫:依歌曲長度排序

Song 是一個 record,已經宣告要實作 Comparable<Song>。完成 compareTo,讓歌曲依秒數由短到長排序;秒數相同時,依歌名排序(字串也有 compareTo)。

你的輸出
預期輸出

重點回顧

  • 介面描述「能做什麼」;類別用 implements 實作,必須寫出所有抽象方法,而且要加 public
  • 一個類別只能繼承一個父類別,但可以實作多個介面;介面型別的變數可以指向任何實作它的物件
  • Java 8 起介面可以有 default 方法(有預設實作,可覆寫)與 static 方法(用介面名稱呼叫);介面的欄位都是 public static final 常數
  • 抽象類別不能 new,可以有欄位、建構子、一般方法與抽象方法;適合關係密切、要共用程式碼的類別
  • 實作 Comparable<T> 的 compareTo(負數排前、0 相同、正數排後),Arrays.sort 就能排序你的物件
  • record(Java 16 起)一行宣告只裝資料的類別,自動產生建構子、存取方法、toString、equals,也可以實作介面
這一課完成了!
Next lesson:例外處理
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。