1. 集合可變性的問題
在 Java 中,集合就像一個貨倉:任何人都可以來加點東西、拿走些東西、或改來改去。有時這很方便,但在大型程式中會變成頭痛的來源。想像一下,你把自己類別中的商品清單傳到外部,結果有人把一半項目刪了。或者——更刺激的是——在多執行緒程式中,一個執行緒新增元素,另一個執行緒在讀取:結果可能出乎意料,錯誤也很難捉(例如 ConcurrentModificationException)。
以下示例說明為什麼集合的可變性是 bug 的來源:
import java.util.*;
public class Inventory {
private List<String> products = new ArrayList<>();
public Inventory() {
products.add("茶");
products.add("咖啡");
}
public List<String> getProducts() {
// 危險!返回了內部清單的參照
return products;
}
}
public class Main {
public static void main(String[] args) {
Inventory inv = new Inventory();
List<String> external = inv.getProducts();
external.remove("茶"); // 糟了!現在庫存裡沒有茶了
System.out.println(inv.getProducts()); // [咖啡]
}
}
看出玄機了嗎?一個方法返回內部集合,另一段程式碼就把它改了。這樣很容易不小心破壞本該被保護的資料。
2. 建立不可變集合:Collections.unmodifiable*
為了避免這類狀況,Java 提供了有效的保護:可以用 Collections 類別中的特殊包裝將集合變成「不可變」:
- Collections.unmodifiableList(list)
- Collections.unmodifiableSet(set)
- Collections.unmodifiableMap(map)
這是怎麼運作的?先建立一個普通集合,然後用「不可變」外殼把它包起來:
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> drinks = new ArrayList<>();
drinks.add("茶");
drinks.add("咖啡");
List<String> immutableDrinks = Collections.unmodifiableList(drinks);
System.out.println(immutableDrinks); // [茶, 咖啡]
// 試著新增元素
immutableDrinks.add("可可"); // 砰!UnsupportedOperationException
}
}
嘗試修改這樣的集合會拋出 UnsupportedOperationException。這就像在盒子上貼了一個巨大的標籤 「請勿觸碰!」——任何想新增或刪除的人都會被阻止(或在呼叫堆疊上被教訓)。
範例:保護內部狀態
讓我們修正前面範例中的 Inventory 類別:
import java.util.*;
public class Inventory {
private List<String> products = new ArrayList<>();
public Inventory() {
products.add("茶");
products.add("咖啡");
}
public List<String> getProducts() {
// 現在返回包裝
return Collections.unmodifiableList(products);
}
}
現在,如果有人嘗試修改取得的清單,就會得到例外。
3. 不可變集合的行為:淺層保護
重要的是要理解:unmodifiableList 與它的「兄弟們」只是把原始集合包起來。它們不會建立副本——對原始集合(包在「裡面」的那個)的任何修改,都會在包裝中看得到!
示範
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> drinks = new ArrayList<>();
drinks.add("茶");
List<String> immutableDrinks = Collections.unmodifiableList(drinks);
drinks.add("咖啡"); // 修改原始集合
System.out.println(immutableDrinks); // [茶, 咖啡] — 元素出現了!
}
}
結論:包裝只會防止透過包裝本身進行修改。如果有人握有原始集合的參照,他仍然可以修改它。
4. 深層不可變:迷思與現實
unmodifiable* 包裝只讓集合在外觀上不可變。但如果集合中包含可變的物件,這些物件仍然可以被修改!
範例
import java.util.*;
class Product {
String name;
Product(String name) {
this.name = name;
}
public String toString() {
return name;
}
}
public class Main {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("茶"));
List<Product> immutableProducts = Collections.unmodifiableList(products);
// 修改集合中的物件
immutableProducts.get(0).name = "咖啡";
System.out.println(immutableProducts); // [咖啡]
}
}
結論:
- 集合是「不可變」的,但其中的物件不是。
- 若要達到完全(深層)的不可變,請使用不可變物件(例如 String、Integer、record 類別,或讓你自己的類別成為 immutable)。
5. 何時使用不可變集合
用於保護內部狀態
如果你的類別儲存了一個集合,並且要對外提供它,一定要返回包裝,避免任何人不小心(或故意)修改你的資料:
public List<String> getProducts() {
return Collections.unmodifiableList(products);
}
在多執行緒程式中
在多執行緒應用中,可變集合是問題的來源(race condition、ConcurrentModificationException 等各種「人生樂事」)。如果集合在建立後不需要再修改——就把它做成不可變的。
在層與層之間傳遞資料
如果你要把集合從程式的一個層級傳遞到另一個層級(例如從 DAO 到 service),請傳遞不可變的副本或包裝——這能避免意外修改。
6. 實用範例
範例 1:保護學生清單
import java.util.*;
public class Group {
private final List<String> students = new ArrayList<>();
public void addStudent(String name) {
students.add(name);
}
public List<String> getStudents() {
return Collections.unmodifiableList(students);
}
}
現在,沒有人能透過 getStudents() 直接新增或刪除學生。
範例 2:不可變的 Map
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, Integer> grades = new HashMap<>();
grades.put("瓦夏", 5);
grades.put("瑪莎", 4);
Map<String, Integer> immutableGrades = Collections.unmodifiableMap(grades);
// immutableGrades.put("彼佳", 3); // UnsupportedOperationException
}
}
7. 有用的細節
現代的替代方案:List.of, Set.of, Map.of
自 Java 9 起,出現了更方便的不可變集合建立方式:
List<String> drinks = List.of("茶", "咖啡");
Set<String> fruits = Set.of("蘋果", "香蕉");
Map<String, Integer> ages = Map.of("瓦夏", 20, "瑪莎", 21);
- 這些集合是不可變的(任何修改嘗試都會丟出例外)。
- 它們沒有「原始」的可變集合(不同於 Collections.unmodifiable*)。
- 不允許 null 值。
另外自 Java 10 起有複製方法:List.copyOf、Set.copyOf、Map.copyOf——會建立傳入集合的不可變副本。
建立不可變集合方式的比較
| 方式 | 深層不可變性 | 能否修改原始集合? | 允許 null? | Java 版本 |
|---|---|---|---|---|
|
否 | 是 | 是 | 1.2 |
|
否 | 否(沒有原始集合) | 否 | 9+ |
8. 使用不可變集合時的常見錯誤
錯誤 #1:在建立包裝後仍修改原始集合。你建立了 unmodifiableList,但之後有人修改了原始清單。包裝無法防止這種情況——變更會在所有使用該包裝的地方顯示出來。
錯誤 #2:期待深層不可變。許多人以為既然集合不可變,那集合內的物件也不能改。實際上保護的是結構(透過集合的新增/刪除/修改),而不是物件內容。
錯誤 #3:在現代工廠方法中使用含 null 的不可變集合。透過 List.of、Set.of、Map.of 建立的集合不允許 null。嘗試新增或取得 null 會導致拋出例外。
錯誤 #4:將可變集合的參照暴露到外部。如果你把內部集合的參照(未經包裝)返回到外部,你就失去了對資料的控制——這是通往 bug 與不變式「洩漏」的捷徑。
錯誤 #5:在期望可變性的程式碼中使用不可變集合。如果外部程式碼嘗試修改集合(例如新增元素),就會得到 UnsupportedOperationException。請確保消費端知道資料是不可變的。
GO TO FULL VERSION