1. record 與 class 的比較:主要差異是什麼?
在 Java 中,我們有兩種主要方式來描述自訂資料型別:一般類別 (class) 與 record 類別 (record)。乍看之下,兩者都能用來儲存並處理資料;但深入一點就會發現,差異比想像中多!
差異表: class vs record
| 特性 | 一般類別 (class) | record 類別 (record) |
|---|---|---|
| 可變性 | 任意:欄位可標示為 final,也可以不是 | 不變:所有欄位皆為 final |
| 繼承 | 可繼承(extends),預設不是 final | 一律為 final,不能作為超類別 |
| 欄位 | 不拘:可為靜態或非靜態、final 或非 final、任意型別 | 僅有 record 元件(private final),外加靜態欄位 |
| Getter/Setter | 需自行撰寫(或用 Lombok 產生) | 自動建立 getter(方法名稱即欄位名稱),沒有 setter |
| equals/hashCode/toString | 通常手動撰寫/工具產生(equals、hashCode、toString) | 依所有元件自動產生 |
| 建構子 | 可任意定義,多個也行 | 僅一個主要建構子(涵蓋所有元件),可新增精簡建構子 |
| 介面 | 可以實作 | 可以實作 |
| 其他方法 | 不拘 | 可新增,但只能是方法(不能新增欄位) |
| 在集合中的使用 | 可以,但必須正確實作 equals/hashCode | 非常適合作為鍵/值,相關實作已就緒 |
範例說明
一般類別:
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}
record 類別:
public record Person(String name, int age) { }
就這樣!一行程式碼——就能得到相同(甚至更好)的效果,而且不會遺漏重要實作。
2. record 類別的限制
record 不是「簡短語法」而已,而是一套有嚴格規範的概念。以下詳細說明。
record 一律為 final
依定義,record 類別一律為 final。也就是說,您無法從 record 建立子類別:
public record Point(int x, int y) { }
// public class ColoredPoint extends Point { } // 編譯錯誤!
若需要擴充行為,請使用一般類別或合成(把 record 放進類別中)。
record 不能作為超類別
record 類別不能作為其他類別的父類別,它一律是 final。這是合理的:如果可以,某人可能加上可變欄位——整個「不可變資料」的概念就會崩潰。
僅有 final 欄位(元件)
所有 record 元件都在標頭中宣告,且預設為 private final。您不能在 record 的主體中新增非靜態欄位:
public record User(String login, String email) {
// int counter; // 錯誤!不可新增非靜態欄位
static int totalUsers = 0; // 可以,這是靜態欄位
}
沒有 setter
record 類別不能為元件提供 setter。任何加入類似 setX(int x) 的方法都沒有意義:您無法在建立物件後變更欄位值。
public record Point(int x, int y) {
// public void setX(int x) { this.x = x; } // 錯誤:無法變更 final 欄位
}
沒有無參數建構子
record 類別始終只有主要建構子,必須接收所有元件的值。無法在未提供所有資料的情況下建立 record:
Point p = new Point(1, 2); // OK
// Point p = new Point(); // 錯誤:沒有無參數建構子
沒有非靜態初始區塊
record 類別不能包含非靜態初始區塊(也就是寫在方法之外的大括號初始區塊):
public record User(String login) {
// { /* ... */ } // 錯誤:禁止非靜態初始區塊
}
繼承方面的限制
record 類別不能顯式繼承其他類別(除了作為所有 record 基底類別且對我們隱藏的 java.lang.Record)。但可以實作介面!
public interface Printable {
void print();
}
public record Book(String title) implements Printable {
@Override
public void print() {
System.out.println("列印書籍: " + title);
}
}
不適合複雜的商業邏輯
record 著重在資料,而非行為。若物件具有複雜邏輯、可變狀態、「生命週期」或大量相依性——record 幫不上忙,改用一般類別更合適。
3. 何時該使用 record 類別?
- DTO (Data Transfer Object): 在應用層、服務、微服務或 REST 控制器之間傳遞不可變資料(例如 JSON 回應)。
- Value Object: 僅由其值所定義的物件。
- 集合中的鍵與值: 當需要正確實作 equals 與 hashCode(例如用於 HashMap 或 Set)。
- 計算結果: 當需要從方法一次回傳多個值(例如,record Pair<T, U>(T first, U second))。
範例:REST 控制器的 DTO
public record UserDto(String login, String email) { }
現在可以放心從控制器回傳此型別的物件,而不必擔心有人會改動其欄位。
範例:HashMap 的鍵
public record Point(int x, int y) { }
Map<Point, String> pointNames = new HashMap<>();
pointNames.put(new Point(1, 2), "A");
pointNames.put(new Point(3, 4), "B");
// 一切運作正常:equals 和 hashCode 已經實作好了!
4. 何時不該使用 record 類別
- 可變狀態: 只要有任一欄位需要在物件建立後變更。
- 複雜邏輯: 物件具有複雜行為、許多方法、以及包含可變狀態的巢狀物件。
- 繼承: 需要類別階層、抽象基底類別、或方法覆寫。
- 商業邏輯的實體: 例如存在於資料庫且具有唯一識別碼的物件。
範例:需要一般類別的情況
public class Account {
private String id;
private int balance;
public Account(String id, int balance) {
this.id = id;
this.balance = balance;
}
public void deposit(int amount) { balance += amount; }
public void withdraw(int amount) { balance -= amount; }
// getters, setters, equals, hashCode, toString...
}
這裡明顯會改變物件狀態——record 不適合。
5. 實務範例:在 record 與 class 之間做選擇
範例 1:record——完美選擇
public record Rectangle(int width, int height) {
public int area() {
return width * height;
}
}
- 矩形僅由寬與高決定。
- 建立後無需變更這些值。
- 可加入實用方法 area()。
- 其餘由 Java 替您處理。
範例 2:class——較佳選擇
public class MutableRectangle {
private int width;
private int height;
public MutableRectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int area() { return width * height; }
}
建立後還需要調整矩形尺寸?使用一般類別。
6. 使用 record 類別時的常見錯誤
錯誤一:嘗試新增非靜態欄位。
record 類別不允許在元件清單之外宣告非靜態欄位。若嘗試這麼做——編譯器會報錯。例如:
public record City(String name) {
// int population; // 錯誤!
}
錯誤二:想要加入 setter。
record 不支援元件的 setter。任何在物件建立後嘗試變更欄位值的行為——都是編譯錯誤。
錯誤三:嘗試繼承 record 或讓 record 繼承其他類別。
record 一律為 final。不能從 record 繼承,record 也不能繼承其他類別(隱含的 java.lang.Record 除外)。
錯誤四:將 record 用於可變物件。
如果計畫在建立後變更物件狀態——record 不適用!請使用一般類別。
錯誤五:忽略建構子的限制。
record 類別必須具有接受所有元件值的建構子。不存在無參數建構子!
GO TO FULL VERSION