CodeGym /课程 /JAVA 25 SELF /可变与不可变集合:区别与应用

可变与不可变集合:区别与应用

JAVA 25 SELF
第 34 级 , 课程 3
可用

1. 引言

简单来说,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+ 的工厂方法)
实现 ArrayListHashSet List.ofSet.ofMap.ofcopyOf

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 到服务层)。
  • 从方法返回集合时,保护其不被修改。
  • 在强调安全性的多线程场景中。

示例:常量数据

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.ofSet.ofMap.of)不允许包含 null

List<String> bad = List.of("a", null); // 在创建时就会抛出 NullPointerException!

8. 方案对比:优缺点

可变集合

可变集合的最大优点是灵活。你可以随时添加、删除元素,并在程序逻辑演进时重组集合。这种方式在需要快速搭建临时结构或动态调整内容时尤其方便。

但这种便利也有代价。可变集合始终存在被意外修改的风险:代码的某处可能不小心改动了数据,从而引发难以定位的错误。在多线程程序中更糟——并发修改很容易导致竞态和意外故障。管理此类数据的生命周期也更复杂:你必须时刻记住谁在何时可能会修改集合。

不可变集合

使用不可变集合更省心。你可以确定:没人会“背着你”改动它们。这让代码更安全,更易于调试和测试,而且这些集合可以在多线程之间方便地传递而无需额外加锁。

缺点是有时需要牺牲一定的性能。若需添加新元素,往往要创建集合的新副本,这会带来额外的内存与时间开销。此外,直接将大型、复杂的结构一次性构建为不可变形式并不方便。通常的做法是先在临时的可变集合中构建,待一切就绪后再“冻结”。

9. 常见错误

错误 № 1:向外部返回可变集合。 如果你从方法返回一个普通的 ArrayList,任何外部代码都可以添加或删除元素。这可能导致难以追踪的缺陷。

错误 № 2:用包装视图代替拷贝。 如果你使用 Collections.unmodifiableList,但原始集合在别处仍会被修改,那么“不可变性”——只是错觉。

错误 № 3:在不可变集合中放入可变对象。 即便集合自身不可变,其元素也可能是可变的。这会导致状态被意外修改。

错误 № 4:向通过工厂方法创建的集合中添加 null 与旧式集合不同,新的工厂方法不允许包含 null——会得到 NullPointerException

错误 № 5:依赖具体实现。 通过 List.ofSet.of 创建的集合不保证具体实现类型(不一定是 ArrayListHashSet)。不要依赖这一点。

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