CodeGym /課程 /JAVA 25 SELF /Mutable vs Immutable 集合:差別與應用

Mutable vs Immutable 集合:差別與應用

JAVA 25 SELF
等級 34 , 課堂 3
開放

1. 介紹

簡單說, mutable 集合就是建立之後還能被修改的集合:可以新增、刪除或變更元素。Immutable(不可變)集合則是建立之後就不能再更動的集合。就像水泥凝固後一樣:你可以看、可以摸,但不能再捏出新的形狀。

Mutable 集合像是拿著鉛筆的筆記本:可以寫、可以擦、可以加上新備忘。Immutable 集合則像是你把頁面護貝起來:之後誰都不能增刪任何內容。

可變(mutable)集合的範例

在 Java 中,幾乎所有標準集合預設都是可變的。例如:

  • ArrayList
  • LinkedList
  • HashSet
  • TreeSet
  • HashMap
  • LinkedHashMap
  • 等等

範例:ArrayList

import java.util.*;

List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.set(1, "Charlie"); // 把 Bob 換成 Charlie
names.remove("Alice");   // 刪除了 Alice
System.out.println(names); // [Charlie]

在這裡我們可以對集合做任何事:新增、刪除、交換元素。當集合需要動態建構時很方便,例如在讀取檔案或使用者輸入的過程中。

2. 不可變(immutable)集合的範例

這些在 Java 9 出現的成員你或許已經見過,但仍需要一些時間適應:

  • List.of(...)
  • Set.of(...)
  • Map.of(...)
  • List.copyOf(collection)
  • Set.copyOf(collection)
  • Map.copyOf(map)

範例:List.of

List<String> planets = List.of("Mercury", "Venus", "Earth", "Mars");
System.out.println(planets); // [Mercury, Venus, Earth, Mars]
planets.add("Jupiter"); // 會拋出 UnsupportedOperationException!

嘗試修改該集合會導致執行期例外。

範例:Collections.unmodifiableList

List<String> modifiable = new ArrayList<>(List.of("a", "b"));
List<String> unmodifiable = Collections.unmodifiableList(modifiable);
unmodifiable.add("c"); // UnsupportedOperationException!

但這裡有陷阱:如果你修改了原始集合,包裝也會跟著變!

modifiable.add("c");
System.out.println(unmodifiable); // [a, b, c] — 元素出現了!

3. mutable 與 immutable 集合的主要差異

屬性 Mutable(可變) Immutable(不可變)
能新增元素嗎? 可以 不行
能刪除元素嗎? 可以 不行
能修改元素嗎? 可以(例如,set 不行
執行緒安全 否(預設) 是(無內部狀態可變)
能加入 null 嗎? 可以(通常) 不行(在 Java 9+ 的工廠方法中)
實作 ArrayListHashSet List.ofSet.ofMap.ofcopyOf

4. 為什麼需要不可變集合?

立刻就會有疑問:既然可變集合那麼靈活,為什麼還需要不可變的?其實原因很多,都與程式碼的安全性、可讀性與可預測性有關。

安全性與防錯

當你把集合對外提供(例如從方法或類別返回)時,你會希望確定沒有人會不小心改動其內容。這在集合包含「重要」資料時尤其關鍵,因為初始化後不應再被修改。

範例:

public class Team {
    private final List<String> players;

    public Team(List<String> players) {
        // 建立不可變的副本,避免任何人竄改名單
        this.players = List.copyOf(players);
    }

    public List<String> getPlayers() {
        return players;
    }
}

現在任何取得球員清單的程式碼,都無法把自己的「朋友」加進去。

執行緒安全

可變集合在多執行緒同時存取時不安全。不可變集合則可以自由在執行緒之間傳遞——沒有人能把它們搞亂。

簡化除錯

如果集合不會改變,你隨時都知道裡面裝的是什麼。不必擔心它會在程式其他地方被「悄悄」改掉。

作為其他集合中的鍵或值

不可變物件是用作 Map 的鍵或 Set 的元素的理想選擇。如果物件在加入後還可能改變,你可能會失去對它的存取(參見 hashCodeequals)。

5. 何時該使用可變集合?

以下情況適合使用可變集合:

  • 集合需要分階段、在迴圈中或從不同來源建構。
  • 需要頻繁變動:新增、刪除、排序。
  • 集合僅供內部使用,外部無法破壞它。

範例:建構清單

List<String> shoppingList = new ArrayList<>();
shoppingList.add("牛奶");
shoppingList.add("麵包");
shoppingList.add("蘋果");
// 建好之後—可以建立不可變版本
List<String> finalList = List.copyOf(shoppingList);

6. 何時該使用不可變集合?

  • 用於保存常數資料(例如:星期幾的清單)。
  • 在應用層之間傳遞集合(例如:從 DAO 到 service)。
  • 從方法返回集合以防止它被修改。
  • 在多執行緒情境中,當安全性很重要時。

範例:常數資料

public static final List<String> WEEKDAYS = List.of(
    "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"
);

範例:對外傳回

public List<String> getReadOnlyNames() {
    return List.copyOf(names); // 沒有人能修改這個清單
}

7. 特性與陷阱

不可變 ≠ 執行緒安全

不可變集合僅防止被修改,但不代表它能避免多執行緒環境中的其他問題(例如集合元素本身是可變物件)。

List<List<String>> listOfLists = List.of(new ArrayList<>());
listOfLists.get(0).add("Oops!"); // 仍可修改內部清單!

包裝 vs 複本

如前所述,Collections.unmodifiableList 是包裝,如果修改原始集合,包裝也會跟著變;而 List.copyOf 則會建立真正獨立的複本。

List<String> base = new ArrayList<>(List.of("a", "b"));
List<String> wrap = Collections.unmodifiableList(base);
List<String> copy = List.copyOf(base);

base.add("c");
System.out.println(wrap); // [a, b, c] — 改變了!
System.out.println(copy); // [a, b] — 保持不變!

NullPointerException

工廠方法(List.ofSet.ofMap.of)不允許加入 null

List<String> bad = List.of("a", null); // 在建立時就會丟出 NullPointerException!

8. 方法比較:優缺點

可變集合

可變集合最大的優勢是靈活。你可以隨時新增、刪除元素,並隨著程式邏輯的發展重建集合。這種做法特別適合快速組裝臨時結構或需要動態變更的情況。

但便利是有代價的。可變集合總是存在被意外改動的風險:程式中的某處可能不小心更改了資料,造成難以察覺的錯誤。在多執行緒程式中情況更糟——並行修改很容易引發競態與意外失敗。管理這些資料的生命週期也更困難:你必須時時記得誰、何時可能會改動集合。

不可變集合

使用不可變集合會更安心。你可以確定:不會有人在你「背後」改它。這讓程式碼更安全、更易於除錯與測試,集合也能在執行緒間方便地傳遞而不需額外鎖定。

缺點是有時需要犧牲效能。如果要新增元素,就得建立集合的新複本,會增加記憶體與時間成本。此外,直接以不可變形式組裝龐大或複雜的結構並不方便。通常會先用暫時的可變集合來建構,等一切就緒再「凍結」。

9. 常見錯誤

錯誤 1:將可變集合直接返回給外部。 如果你從方法返回一個普通的 ArrayList,任何外部程式碼都能新增或刪除元素。這可能導致很難追蹤的錯誤。

錯誤 2:用包裝取代複本。 如果你使用 Collections.unmodifiableList,但原始集合在其他地方仍會被修改,那麼「不可變」只是幻覺。

錯誤 3:不可變集合內的元素卻是可變物件。 即使集合本身不可變,其中的元素仍可能是可變的。這會導致狀態出現意料之外的變化。

錯誤 4:嘗試將 null 加入由工廠方法建立的集合。 與舊的集合不同,新工廠方法不允許加入 null——你會得到 NullPointerException

錯誤 5:假定具體實作類型。 透過 List.ofSet.of 建立的集合並不保證具體的實作類型(不一定是 ArrayListHashSet)。不要依賴這一點。

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