CodeGym /課程 /JAVA 25 SELF /Record:語法與優點

Record:語法與優點

JAVA 25 SELF
等級 22 , 課堂 0
開放

1. DTO 類別的問題:我們為什麼需要 record 類別?

說實話,你寫過多少次這樣的類別?


public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point{" + "x=" + x + ", y=" + y + '}';
    }
}

看起來不難,但數數看有多少行!再想像一下:如果你有 10 個這樣的類別,每個有 5–6 個欄位。連 IDE 都會對產生這些樣板程式碼感到疲倦。若你決定加一個新欄位——還得改建構子、getter、equalshashCodetoString…… 枯燥、重複,還是錯誤的溫床。

這類類別稱為 DTO(Data Transfer Object)或 Value Object。 它們只是用來存放資料——就這樣。但因為樣板程式碼太多,維護起來很辛苦。

如果你覺得這不算問題,不妨等到你得同時修改 50 個這樣的類別時再說。到時你會特別懷念 record 類別!

2. 認識 record:語法與 Java 16+ 的魔法

自從 Java 16 起,一切都改變了。出現了一種新的類別——record。它專為只需保存一組資料的情境設計。語法幾乎就像其他語言的元組(tuple)。

如何宣告 record?


public record Point(int x, int y) { }

就這樣!你剛建立了一個不可變的類別,包含兩個欄位、建構子、getter、equalshashCodetoString,完全不需要多餘的樣板。

Java 在「幕後」做了什麼?

  • 欄位 xy 會成為 private final
  • 存取方法會自動產生:int x()int y()
  • 建構子public Point(int x, int y)
  • equals/hashCode:依值比較所有欄位。
  • toString:回傳像 "Point[x=1, y=2]" 這樣的字串。

可以說,record 是「加強版的 DTO」:程式碼更少、保證更多、錯誤更少。

不可變性(immutability)

record 類別的所有欄位都會自動是 final。物件建立之後就無法再被修改——編譯器會替你把關。

如果你嘗試加入 setter 或讓欄位不是 final——編譯器會阻止你。這種為你著想的貼心防護並不常見!

3. 使用 record 類別的範例

一般類別(很多程式碼):


public class Client {
    private final String name;
    private final int id;

    public Client(String name, int id) {
        this.name = name;
        this.id = id;
    }

    public String getName() { return name; }
    public int getId() { return id; }

    // equals, hashCode, toString ...
}

record 類別(只要一行!):


public record Client(String name, int id) { }

使用方式:


public class Main {
    public static void main(String[] args) {
        Client client = new Client("Ivan", 123);
        System.out.println(client.name()); // Ivan
        System.out.println(client.id());   // 123
        System.out.println(client);        // Client[name=Ivan, id=123]
    }
}

請注意:
- 存取方法名稱與欄位相同:name()id()
- 沒有 setName()setId()——物件在建立後無法被修改。

4. record 類別的優點:更少程式碼、更少錯誤

程式碼更少——更幸福

為何要寫 40 行,能用一行就好?Record 類別能節省時間與心力,特別是在有大量 DTO 與 value 物件的大型專案中。

以契約保證的不可變

  • record 類別一律是 final 且不可變。
  • 物件不會被偽造或不小心被修改。
  • 不會因為物件狀態在意想不到的地方被改變而出現「怪 bug」。
  • 可安全用於多執行緒程式(前提是所有欄位本身也是 immutable)。

自動產生 equals/hashCode/toString

不必手寫比較、雜湊與友善字串輸出的方法。一切都會自動且正確地完成。


Client c1 = new Client("Anna", 42);
Client c2 = new Client("Anna", 42);

System.out.println(c1.equals(c2));                 // true
System.out.println(c1.hashCode() == c2.hashCode()); // true
System.out.println(c1);                             // Client[name=Anna, id=42]

對集合與鍵值使用再適合不過

可以把 record 物件當作 HashMap 的鍵、HashSet 的元素等——一切都會正確運作,因為 equalshashCode 會涵蓋所有欄位。


import java.util.HashMap;
import java.util.Map;

Map<Client, String> clients = new HashMap<>();
clients.put(new Client("Anna", 42), "VIP");

System.out.println(clients.get(new Client("Anna", 42))); // VIP

清楚描述資料

record 的語法會直接顯示儲存哪些資料,且物件是不可變的。這使得程式碼對其他開發者(以及半年後的你)更容易理解。

5. 表格:一般類別與 record 類別的比較

一般類別 record 類別
語法 許多程式碼 一行
不可變性 需要自行明確撰寫 由編譯器保證
方法自動產生 是(equals, hashCode, toString)
可否新增欄位 可以 只能新增 record 組件
繼承 可以被繼承 一律為 final,無法被繼承
在集合中的使用 需正確實作相關方法 開箱即用

6. 擴充教學應用:以 record 類別示範

假設你的教學用銀行應用需要儲存帳戶交易:日期、金額、操作類型(例如「存入」或「提領」)。

在 Java 16 之前:


public class Transaction {
    private final LocalDate date;
    private final double amount;
    private final String type;

    public Transaction(LocalDate date, double amount, String type) {
        this.date = date;
        this.amount = amount;
        this.type = type;
    }

    public LocalDate getDate() { return date; }
    public double getAmount() { return amount; }
    public String getType() { return type; }

    // equals, hashCode, toString ...
}

使用 Java 16 與 record:


import java.time.LocalDate;

public record Transaction(LocalDate date, double amount, String type) { }

使用方式:


Transaction t = new Transaction(LocalDate.now(), 100.0, "deposit");
System.out.println(t);         // Transaction[date=2024-06-01, amount=100.0, type=deposit]
System.out.println(t.amount()); // 100.0

7. 視覺化:record 會生成什麼

來看看「展開後」的 record 類別(大致上是編譯器生成的內容):


public final class Point extends java.lang.Record {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int x() { return x; }
    public int y() { return y; }

    @Override
    public boolean equals(Object o) { /* 依欄位比較 */ }

    @Override
    public int hashCode() { /* 依欄位計算 */ }

    @Override
    public String toString() { /* 友善的字串輸出 */ }
}

簡而言之:何時該使用 record

  • 當你需要一個不可變的資料承載物件。
  • 當你想要「可靠」的 equals/hashCode/toString,且不必手寫。
  • 當你在做 DTO、Value Object、二元組、三元組、顏色、點、區間、作為集合的鍵等情境。

8. 使用 record 類別時的常見錯誤

錯誤一:嘗試加入 setter 或在建立後修改欄位。
record 類別不允許修改其欄位。如果你嘗試加入像 setX(int x) 之類的方法,編譯器會立刻表示「不允許」。直接嘗試修改欄位也一樣。

錯誤二:嘗試加入非靜態欄位。
在 record 類別中只能宣告「組件」(名稱後括號中的欄位)以及靜態欄位。一般的非靜態欄位一律不允許——編譯器會阻止你。

錯誤三:用 record 承載可變的邏輯。
record 類別並非用於具可變狀態的物件。若建立後需要再改變,請使用一般類別。

錯誤四:忘了 record 永遠是 final。
record 類別無法被繼承,也不能作為父類別。嘗試違反這項限制會導致編譯錯誤。重點是——不要試圖「擴充」 record;它的設計就是完整且不可變的型別。

錯誤五:忽略自動生成的方法。
若你要覆寫 equalshashCodetoString,務必小心——不要破壞它們的契約,否則集合與比較將會失常。

1
任務
JAVA 25 SELF, 等級 22, 課堂 0
上鎖
建立數位書籍卡片 📚
建立數位書籍卡片 📚
1
任務
JAVA 25 SELF, 等級 22, 課堂 0
上鎖
查看學生資料 🎓
查看學生資料 🎓
1
任務
JAVA 25 SELF, 等級 22, 課堂 0
上鎖
檢查地圖上城市的相同性 🗺️
檢查地圖上城市的相同性 🗺️
1
任務
JAVA 25 SELF, 等級 22, 課堂 0
上鎖
應用程式中的使用者存取管理 🔑
應用程式中的使用者存取管理 🔑
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION