1. 簡介
經典集合:靈活與陷阱
當你使用 new ArrayList<>() 建立集合時,你會得到一個能自由修改的結構:可新增、刪除、變更元素。當你「臨時」建構資料時,這很方便。但如果你把這個集合傳給其他類別或方法,而那裡不應該修改它呢?又或者你不小心把集合曝露到外部,有人改動了它?這時問題就來了。
典型錯誤範例
import java.util.*;
public class Example {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
// 將集合傳遞到 "外部"
processNames(names);
// 以為清單沒有被改動...
System.out.println(names);
}
public static void processNames(List<String> list) {
// 結果有人把元素刪掉了!
list.remove("Bob");
}
}
輸出:
[Alice, Charlie]
你的集合被改了,儘管你完全沒有這個打算。在大型專案中,這種「驚喜」很容易演變成極其惱人且難以追蹤的 bug。
危險在於,與集合互動的程式碼可能會突然面對資料的不可預期變化。此外,總是存在資訊遺失的風險——有人不小心刪了元素或覆寫了它。若集合同時被多個執行緒修改,不僅可能遇到 ConcurrentModificationException,還可能碰到更狡猾的問題——資料不一致。
保護集合:舊方法
在 Java 9 之前,只能使用像 Collections.unmodifiableList(...) 這樣的包裝方法(我們在上一課提到過)。它們能回傳「凍結」的集合。但這種方式不總是好用,也無法解決所有問題(細節將在下一堂課說明)。
2. 現代解法:工廠方法 List.of、Set.of、Map.of
在 Java 9,集合介面中新增了靜態方法:List.of、Set.of、Map.of。它們能快速便利地建立不可變的集合。就好像你建立好集合後,立刻把它澆進混凝土——沒有人可以新增、刪除或變更元素。
建立不可變集合的範例
import java.util.*;
public class ImmutableDemo {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");
Set<Integer> numbers = Set.of(1, 2, 3);
Map<String, Integer> ages = Map.of("Alice", 30, "Bob", 25, "Charlie", 28);
System.out.println(names);
System.out.println(numbers);
System.out.println(ages);
}
}
輸出:
[Alice, Bob, Charlie]
[1, 2, 3]
{Alice=30, Bob=25, Charlie=28}
它如何運作?
- List.of(...) — 建立不可變的 List。
- Set.of(...) — 建立不可變的 Set。
- Map.of(...) — 建立不可變的 Map(最多 10 組鍵值對;更多請使用 Map.ofEntries(...))。
注意! 由這些方法建立的集合不允許修改。任何新增、刪除或取代元素的嘗試都會拋出例外。
3. 使用範例與「陷阱」
範例:嘗試修改集合
import java.util.*;
public class ImmutableFail {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob");
// names.add("Charlie"); // 執行期錯誤!
try {
names.add("Charlie");
} catch (UnsupportedOperationException ex) {
System.out.println("不可新增元素: " + ex.getClass().getSimpleName());
}
}
}
輸出:
不可新增元素: UnsupportedOperationException
範例:嘗試加入 null
import java.util.*;
public class NullFail {
public static void main(String[] args) {
try {
List<String> badList = List.of("Alice", null, "Bob");
} catch (NullPointerException ex) {
System.out.println("不允許 Null: " + ex.getClass().getSimpleName());
}
}
}
輸出:
不允許 Null: NullPointerException
範例:Set.of 中的重複項
import java.util.*;
public class DuplicatesFail {
public static void main(String[] args) {
try {
Set<String> badSet = Set.of("one", "two", "one");
} catch (IllegalArgumentException ex) {
System.out.println("不允許重複: " + ex.getClass().getSimpleName());
}
}
}
輸出:
不允許重複: IllegalArgumentException
範例:大量配對的 Map.of
import java.util.*;
public class MapOfLarge {
public static void main(String[] args) {
// Map.of 最多支援 10 組鍵值對
Map<String, Integer> map = Map.of(
"one", 1, "two", 2, "three", 3, "four", 4, "five", 5,
"six", 6, "seven", 7, "eight", 8, "nine", 9, "ten", 10
);
System.out.println(map);
// 若超過請使用 Map.ofEntries
Map<String, Integer> bigMap = Map.ofEntries(
Map.entry("eleven", 11),
Map.entry("twelve", 12),
Map.entry("thirteen", 13)
// ...等等
);
System.out.println(bigMap);
}
}
4. 不可變集合的特性與限制
不可修改。
任何新增、刪除或變更元素的嘗試都會導致 UnsupportedOperationException。即使是通常可用的方法(add、remove、set)也不會運作。
不可使用 null。
如果你嘗試將 null 作為 List 或 Set 的元素,或作為 Map 的鍵/值,都會得到 NullPointerException。這是為了安全:null 元素常導致集合中的錯誤。
不保證具體實作。
你不會知道透過 List.of 等方法建立的集合底下究竟是哪個類別。不要進行 instanceof ArrayList 或嘗試把集合轉型成某個具體型別。
元素順序。
— 在 List.of 中,會保留元素順序(如同一般 List)。
— 在 Set.of 中,不保證順序(實務上可能與參數傳入順序一致,但不應依賴它)。
— 在 Map.of 中,不保證配對順序。
效能。
透過工廠方法建立的集合通常比對可變集合做不可修改包裝更快,因為它們不會為多餘的功能耗費記憶體。
5. 何時以及為何使用不可變集合
常數資料集
如果你有在程式執行期間不應變動的 List、Set 或 Map,請使用 List.of、Set.of、Map.of。例如:
private static final List<String> ROLES = List.of("USER", "ADMIN", "MODERATOR");
現在任何人都無法向此清單添加多餘的角色。
從方法回傳集合
如果你要從方法回傳集合,且不希望它在外部被修改:
public List<String> getDefaultNames() {
return List.of("Alice", "Bob", "Charlie");
}
取得者無法破壞你的資料。
在應用分層之間傳遞
當你在程式的不同部位之間傳遞集合(例如在 Web 應用中,於 Controller 與 Service 之間),最好使用不可變集合,避免任何人「悄悄地」修改它們。
安全與執行緒安全
不可變集合天生在讀取方面是執行緒安全的:既然沒人能修改它們,就可以在多個執行緒中放心地不加同步地讀取。
6. 通用應用的實務範例
假設在我們的教學應用中有支援指令的清單:
public class Commands {
public static final List<String> SUPPORTED_COMMANDS = List.of(
"help", "exit", "list", "add", "remove"
);
}
如果你嘗試這樣做:
Commands.SUPPORTED_COMMANDS.add("hack_the_system");
你會得到例外,無法對應用造成傷害。
或者,例如你有一個錯誤代碼的 Map:
public class ErrorCodes {
public static final Map<Integer, String> CODES = Map.of(
404, "Not Found",
500, "Internal Server Error",
403, "Forbidden"
);
}
任何嘗試新增新的代碼——都會拋出例外。
7. 建立集合方法的比較
| 建立方式 | 可修改? | 可使用 null? | 可重複? | 執行緒安全 | 範例 |
|---|---|---|---|---|---|
|
是 | 是 | 是 | 否 | |
|
否 | 否 | 是 | 是* | |
|
否 | 否 | 否 | 是* | |
|
否 | 否 | 否 | 是* | |
|
否 | 取決於來源 | 是 | 否 | |
* — 執行緒安全僅限於不可變性:只要集合不被修改,就能在多執行緒中安全讀取。
8. 使用 List.of, Set.of, Map.of 的常見錯誤
錯誤 №1:嘗試修改集合。
最常見的錯誤是嘗試對透過 List.of、Set.of 或 Map.of 建立的集合新增或刪除元素。例如,names.add("Dmitry") 或 ages.remove("Bob")。這必定在執行期導致 UnsupportedOperationException。
錯誤 №2:嘗試加入 null。
如果你不小心把 null 傳給任一方法(例如,List.of("Alice", null)),就會得到 NullPointerException。Java 9+ 的不可變集合不喜歡 null——這其實是好事。
錯誤 №3:在 Set.of 或 Map.of 中出現重複元素。
Set.of("a", "b", "a") 或 Map.of("x", 1, "x", 2) 會導致 IllegalArgumentException。依定義,Set 與 Map 不能包含重複。
錯誤 №4:期待具體的實作類別。
不要這樣做:
List<String> list = List.of("a", "b");
if (list instanceof ArrayList) {
// ...
} // 這永遠是 false!
內部實作是隱藏的——不要依賴實作細節。
錯誤 №5:嘗試使用修改集合的方法。
即便是 clear()、set(index, value)(對 List 而言)這類方法,也會拋出例外。請記住:透過工廠方法建立的集合不可變。
GO TO FULL VERSION