CodeGym /課程 /JAVA 25 SELF /List.of, Set.of, Map.of — 不可變集合

List.of, Set.of, Map.of — 不可變集合

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

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.ofSet.ofMap.of

在 Java 9,集合介面中新增了靜態方法:List.ofSet.ofMap.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。即使是通常可用的方法(addremoveset)也不會運作。

不可使用 null。
如果你嘗試將 null 作為 List 或 Set 的元素,或作為 Map 的鍵/值,都會得到 NullPointerException。這是為了安全:null 元素常導致集合中的錯誤。

不保證具體實作。
你不會知道透過 List.of 等方法建立的集合底下究竟是哪個類別。不要進行 instanceof ArrayList 或嘗試把集合轉型成某個具體型別。

元素順序。
— 在 List.of 中,會保留元素順序(如同一般 List)。
— 在 Set.of 中,不保證順序(實務上可能與參數傳入順序一致,但不應依賴它)。
— 在 Map.of 中,不保證配對順序。

效能。
透過工廠方法建立的集合通常比對可變集合做不可修改包裝更快,因為它們不會為多餘的功能耗費記憶體。

5. 何時以及為何使用不可變集合

常數資料集

如果你有在程式執行期間不應變動的 List、Set 或 Map,請使用 List.ofSet.ofMap.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? 可重複? 執行緒安全 範例
new ArrayList<>()
new ArrayList<>()
List.of(...)
是*
List.of("a", "b")
Set.of(...)
是*
Set.of("a", "b")
Map.of(...)
是*
Map.of("a", 1, "b", 2)
Collections.unmodifiableList(...)
取決於來源
Collections.unmodifiableList(list)

* — 執行緒安全僅限於不可變性:只要集合不被修改,就能在多執行緒中安全讀取。

8. 使用 List.of, Set.of, Map.of 的常見錯誤

錯誤 №1:嘗試修改集合。
最常見的錯誤是嘗試對透過 List.ofSet.ofMap.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 而言)這類方法,也會拋出例外。請記住:透過工廠方法建立的集合不可變

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