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(...) —— 创建不可变列表。
  • Set.of(...) —— 创建不可变集合。
  • 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 作为列表或集合的元素,或作为 Map 的键/值加入,会得到 NullPointerException。这是出于安全考虑:包含 null 的元素常常导致集合中的错误。

不保证具体实现。
通过 List.of 等创建的集合,其底层具体类不可知。不要对它做 instanceof ArrayList 或尝试强制转换为某个具体类型。

元素顺序。
— 对于 List.of,元素顺序会被保留(就像普通列表)。
— 对于 Set.of,不保证顺序(实践中可能与传入参数顺序一致,但不要依赖)。
— 对于 Map.of,键值对的顺序不保证。

性能。
通过工厂方法创建的集合通常比对可变集合的包装器更高效,因为它们不会为多余的可变能力浪费内存。

5. 何时以及为何使用不可变集合

常量数据集

如果你有在程序运行期间不应改变的列表、集合或 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。按定义,集合与 Map 的键都不能重复。

错误 №4:期望某个具体实现。
不要这样做:

List<String> list = List.of("a", "b");
if (list instanceof ArrayList) { 
    // ...
} // 这始终为 false!

内部实现是隐藏的——不要依赖实现细节。

错误 №5:尝试调用修改集合的方法。
即便是 clear()、(列表的)set(index, value) 等方法也会抛出异常。记住:通过工厂方法创建的集合是不可变的。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION