1. record 類別與不可變性
不可變性——指的是物件的一種特性:其狀態在建立之後就無法再被改變。換句話說,一旦物件被建立,你就不能修改它的內部資料。就這樣!像刻在石頭上一樣。
想像一張火車票。一旦票印出來,你就不能再更改出發日期或地點(除非你打算用修圖軟體做些小把戲)。票就是一個不可變的物件。如果你想要另一張票——就再買一張新的。
在程式設計中,這類物件稱為 immutable objects。它們能保護程式免於被意外修改,讓程式碼更安全、行為更可預測。
不可變物件的特徵
- 物件的所有欄位皆為 final(只能被指定一次,通常在建構子中)。
- 沒有 setter(不提供會修改欄位值的方法)。
- 所有用來回傳內部資料的方法,要嘛回傳複本,要嘛確保回傳的資料本身也是不可變的。
在 Java 中,record 類別的設計初衷就是讓你能簡單、輕鬆地建立不可變的資料結構。
為什麼 record 是不可變的?
- record 的每個 component 都是 final。
當你宣告一個 record 時,編譯器會自動將其所有欄位設為 private 與 final。這表示物件建立後,你無法再修改其欄位。 - 沒有 setter。
在 record 類別中不能新增像 setX(int x) 這樣的方法——編譯器不允許你在物件建立後再變更欄位值。 - 建構子只會指定一次值。
所有值都只在建立物件的那一刻被設定。
範例
public record Point(int x, int y) {}
Point p = new Point(5, 10);
// p.x = 7; // 編譯錯誤:欄位 x 為 private 且為 final
// p.x(7); // 錯誤:沒有 setter!
System.out.println(p.x()); // 5
嘗試修改欄位或呼叫不存在的 setter 都會導致編譯錯誤。Java 會嚴格維護不可變性的合約。
2. 不可變物件的優勢
多執行緒環境中的安全性
在多執行緒的程式中(現在大多數程式都是!)不可變物件就像防彈背心。若物件不可被修改,不同的執行緒就能安心地同時讀取它,而不必擔心有人正在改動它的資料。不需要額外同步,也不必為資料競爭而煩惱。
事實:標準 Java 函式庫中,許多在多執行緒情境中常用的類別,要嘛本身就是不可變的,要嘛已特別處理以避免被修改。
更容易理解與維護的程式碼
如果物件是不可變的,你就能確定:把它傳給其他方法或類別——它不會在「你看不到的地方」被改變。這大幅降低理解與除錯的難度。不用再猜到底是誰在哪裡改了欄位——沒有人能改!
作為集合鍵時更可靠
不可變物件非常適合拿來當作像 HashMap 或 HashSet 這類集合的鍵。為什麼?因為它們的 equals 與 hashCode 只依賴不會改變的欄位。這代表物件不會因為狀態變動而在集合中「失蹤」。
更少隱性 bug
可變物件很容易因為不小心把參考傳錯地方而被改壞。不可變物件就像紙本書:沒有人能把頁面撕掉或重寫。
3. 與一般類別的比較
我們來比較一下一般類別與 record 類別的行為。以下用平面上的一個點作為簡單範例。
一般類別(mutable)
public class PointClass {
private int x;
private int y;
public PointClass(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
}
你可以先建立物件,之後再任意次數地改變它的狀態:
PointClass p = new PointClass(1, 2);
p.setX(10); // p.x 現在是 10
record 類別(immutable)
public record Point(int x, int y) {}
你建立好物件——它就永遠維持當時的狀態:
Point p = new Point(1, 2);
// p.x = 10; // 錯誤!無法直接存取欄位
// p.x(10); // 錯誤!沒有 setter
表格:行為比較
| 一般類別 | record 類別 | |
|---|---|---|
| 欄位 | 任意 | 只能是 private final |
| Setter | 可以新增 | 不能新增 |
| 可變性 | 可變(mutable) | 不可變 |
| 自動產生 | 否 | 是(equals、hashCode、toString) |
4. 實作:如何運用 record 類別的不可變性
來看一個小範例。假設我們在做銀行應用程式,想要保存交易的資訊:
public record Transaction(String fromAccount, String toAccount, double amount) {}
建立物件:
Transaction t = new Transaction("12345", "67890", 1500.0);
System.out.println(t);
// Transaction[fromAccount=12345, toAccount=67890, amount=1500.0]
試著把錢「轉」到另一個帳戶:
// t.toAccount = "11111"; // 錯誤!欄位為 final,且無法存取
// t.toAccount("11111"); // 錯誤!沒有 setter
如果需要另一筆交易——就建立一個新物件:
Transaction t2 = new Transaction(t.fromAccount(), "11111", t.amount());
重要:不可變並不代表「不方便」。這只是另一種風格:當你需要新的狀態時——就建立新的物件。
5. 不可變性的細節:需要記住什麼
不可變性並非絕對!
record 類別保證其欄位不會改變。但如果欄位是指向可變物件(例如陣列或一般類別)的參考,那麼該物件的內容仍然可能被改變。
陣列範例
public record DataHolder(int[] data) {}
int[] arr = {1, 2, 3};
DataHolder holder = new DataHolder(arr);
arr[0] = 99;
System.out.println(holder.data()[0]); // 99!陣列的內容被改變了
結論:如果你想要真正的不可變性,請只使用不可變型別(如 String、Integer、其他 record 等),或者在 record 類別的 canonical constructor 中對可變物件做防禦性複製。例如:
int[] copy = Arrays.copyOf(data, data.length);
6. 自己把一般類別做成不可變
如果你想把一般類別做成 immutable(不可變),需要手動做到:
- 將所有欄位標記為 private final,
- 不要新增 setter,
- 所有欄位僅透過建構子初始化,
- 若欄位是可變物件,請做防禦性複製(defensive copy)。
範例
public final class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String name() {
return name;
}
public int age() {
return age;
}
}
你會同意,用 record 類別可以更簡潔地達成同樣的目的:
public record User(String name, int age) {}
7. 使用不可變 record 類別時的常見錯誤
錯誤一:嘗試在建立後修改欄位。
新手常會在 record 物件上寫出 p.x = 42; 或 p.x(42);。但編譯器會立刻告訴你:「不行!欄位是 final,也沒有 setter」。
錯誤二:把可變物件當作 record 的 component。
如果你在 record 中放入 List、Map、陣列或其他可變物件作為欄位,那麼 record 本身並不能阻止這些物件內容被修改。舉例來說,若你有 record User(List<String> hobbies),別人可以對該清單新增或刪除元素,這會改變你的 record 物件的狀態。為避免這種情況,請使用不可變集合(List.copyOf、Collections.unmodifiableList),或在 record 的建構子中對集合做複製。
錯誤三:對不可變性的誤解。
有些人以為只要是 record 就能防止任何變更。事實上,如果欄位是指向可變物件的參考,其內容仍可被修改,這會導致出乎意料的 bug。
GO TO FULL VERSION