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+ 的工廠方法中) |
| 實作 | ArrayList、HashSet 等 | List.of、Set.of、Map.of、copyOf |
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 的元素的理想選擇。如果物件在加入後還可能改變,你可能會失去對它的存取(參見 hashCode 與 equals)。
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.of、Set.of、Map.of)不允許加入 null:
List<String> bad = List.of("a", null); // 在建立時就會丟出 NullPointerException!
8. 方法比較:優缺點
可變集合
可變集合最大的優勢是靈活。你可以隨時新增、刪除元素,並隨著程式邏輯的發展重建集合。這種做法特別適合快速組裝臨時結構或需要動態變更的情況。
但便利是有代價的。可變集合總是存在被意外改動的風險:程式中的某處可能不小心更改了資料,造成難以察覺的錯誤。在多執行緒程式中情況更糟——並行修改很容易引發競態與意外失敗。管理這些資料的生命週期也更困難:你必須時時記得誰、何時可能會改動集合。
不可變集合
使用不可變集合會更安心。你可以確定:不會有人在你「背後」改它。這讓程式碼更安全、更易於除錯與測試,集合也能在執行緒間方便地傳遞而不需額外鎖定。
缺點是有時需要犧牲效能。如果要新增元素,就得建立集合的新複本,會增加記憶體與時間成本。此外,直接以不可變形式組裝龐大或複雜的結構並不方便。通常會先用暫時的可變集合來建構,等一切就緒再「凍結」。
9. 常見錯誤
錯誤 1:將可變集合直接返回給外部。 如果你從方法返回一個普通的 ArrayList,任何外部程式碼都能新增或刪除元素。這可能導致很難追蹤的錯誤。
錯誤 2:用包裝取代複本。 如果你使用 Collections.unmodifiableList,但原始集合在其他地方仍會被修改,那麼「不可變」只是幻覺。
錯誤 3:不可變集合內的元素卻是可變物件。 即使集合本身不可變,其中的元素仍可能是可變的。這會導致狀態出現意料之外的變化。
錯誤 4:嘗試將 null 加入由工廠方法建立的集合。 與舊的集合不同,新工廠方法不允許加入 null——你會得到 NullPointerException。
錯誤 5:假定具體實作類型。 透過 List.of 或 Set.of 建立的集合並不保證具體的實作類型(不一定是 ArrayList 或 HashSet)。不要依賴這一點。
GO TO FULL VERSION