CodeGym /課程 /JAVA 25 SELF /Immutability — record 類別的不可變性

Immutability — record 類別的不可變性

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

1. record 類別與不可變性

不可變性——指的是物件的一種特性:其狀態在建立之後就無法再被改變。換句話說,一旦物件被建立,你就不能修改它的內部資料。就這樣!像刻在石頭上一樣。

想像一張火車票。一旦票印出來,你就不能再更改出發日期或地點(除非你打算用修圖軟體做些小把戲)。票就是一個不可變的物件。如果你想要另一張票——就再買一張新的。

在程式設計中,這類物件稱為 immutable objects。它們能保護程式免於被意外修改,讓程式碼更安全、行為更可預測。

不可變物件的特徵

  • 物件的所有欄位皆為 final(只能被指定一次,通常在建構子中)。
  • 沒有 setter(不提供會修改欄位值的方法)。
  • 所有用來回傳內部資料的方法,要嘛回傳複本,要嘛確保回傳的資料本身也是不可變的。

Java 中,record 類別的設計初衷就是讓你能簡單、輕鬆地建立不可變的資料結構。

為什麼 record 是不可變的?

  • record 的每個 component 都是 final。
    當你宣告一個 record 時,編譯器會自動將其所有欄位設為 privatefinal。這表示物件建立後,你無法再修改其欄位。
  • 沒有 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 函式庫中,許多在多執行緒情境中常用的類別,要嘛本身就是不可變的,要嘛已特別處理以避免被修改。

更容易理解與維護的程式碼

如果物件是不可變的,你就能確定:把它傳給其他方法或類別——它不會在「你看不到的地方」被改變。這大幅降低理解與除錯的難度。不用再猜到底是誰在哪裡改了欄位——沒有人能改!

作為集合鍵時更可靠

不可變物件非常適合拿來當作像 HashMapHashSet 這類集合的鍵。為什麼?因為它們的 equalshashCode 只依賴不會改變的欄位。這代表物件不會因為狀態變動而在集合中「失蹤」。

更少隱性 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) 不可變
自動產生 是(equalshashCodetoString

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!陣列的內容被改變了

結論:如果你想要真正的不可變性,請只使用不可變型別(如 StringInteger、其他 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 中放入 ListMap、陣列或其他可變物件作為欄位,那麼 record 本身並不能阻止這些物件內容被修改。舉例來說,若你有 record User(List<String> hobbies),別人可以對該清單新增或刪除元素,這會改變你的 record 物件的狀態。為避免這種情況,請使用不可變集合(List.copyOfCollections.unmodifiableList),或在 record 的建構子中對集合做複製。

錯誤三:對不可變性的誤解。
有些人以為只要是 record 就能防止任何變更。事實上,如果欄位是指向可變物件的參考,其內容仍可被修改,這會導致出乎意料的 bug。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION