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、equals、hashCode、toString…… 枯燥、重複,還是錯誤的溫床。
這類類別稱為 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、equals、hashCode 和 toString,完全不需要多餘的樣板。
Java 在「幕後」做了什麼?
- 欄位 x 與 y 會成為 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 的元素等——一切都會正確運作,因為 equals 與 hashCode 會涵蓋所有欄位。
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;它的設計就是完整且不可變的型別。
錯誤五:忽略自動生成的方法。
若你要覆寫 equals、hashCode 或 toString,務必小心——不要破壞它們的契約,否則集合與比較將會失常。
GO TO FULL VERSION