Java List.copyOf 怎样创建不可变副本
来源:17golang原创
时间:2026-10-18 13:37:04 292浏览 收藏
在 Java 方法之间传递一组配置、权限或查询条件时,如果直接把 ArrayList 交给下游代码,接收方就可能改变原列表。List.copyOf 适合解决这个边界:它按源集合的迭代顺序创建一个不接受增删改的列表,源集合之后再追加元素也不会改变这个列表的结构。
官方地址:https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/List.html
List.copyOf(source)返回不可修改列表,调用add、remove或set会抛出UnsupportedOperationException。- 输入集合或其中任意元素为
null时,创建副本会抛出NullPointerException。 - 它主要隔离列表结构和源集合,不会递归复制元素对象;元素自身可变时,仍要单独设计对象边界。
List.copyOf 到底复制了什么
List.copyOf 接收的是 Collection extends E>,返回一个包含相同元素的 List。元素会按照源集合迭代器产生的顺序进入结果列表,所以源集合是 LinkedHashSet 或有明确顺序的列表时,顺序也会被保留下来。这里的“副本”首先指列表容器的独立状态,不等于对每个元素做深拷贝。
import java.util.ArrayList;
import java.util.List;
public class ListCopyOfDemo {
public static void main(String[] args) {
var source = new ArrayList();
source.add("read"); // 先在可变源集合中准备权限
source.add("write");
var snapshot = List.copyOf(source); // 按迭代顺序创建不可变副本
source.add("delete"); // 源集合继续变化,不会把新元素推入 snapshot
System.out.println(source); // [read, write, delete]
System.out.println(snapshot); // [read, write]
}
}
这个结果适合用在“调用方继续组装,接收方只读”的场景。例如控制器收到一组筛选字段后,可以在进入服务层前转成副本;服务层无需担心某个辅助方法偷偷调用 clear 或 add 改掉调用方的数据。

副本不能改,元素对象却不一定不可变
Oracle API 对 List.of 和 List.copyOf 创建的列表都规定了不可修改、禁止 null 元素等特性。这里的不可修改是列表结构层面的约束:不能替换位置上的引用,也不能增加或移除元素。若元素是一个可变对象,仍然可以通过元素引用修改它的内部字段。
import java.util.ArrayList;
import java.util.List;
record MutableLabel(StringBuilder text) {}
public class ElementBoundaryDemo {
public static void main(String[] args) {
var label = new MutableLabel(new StringBuilder("draft"));
var snapshot = List.copyOf(new ArrayList(List.of(label))); // 只固定列表结构
label.text().append("-changed"); // 元素内部仍可变,副本能观察到同一个对象的变化
System.out.println(snapshot.get(0).text()); // draft-changed
// 列表位置不能替换;这行会抛出 UnsupportedOperationException。
// snapshot.set(0, new MutableLabel(new StringBuilder("new")));
}
}
因此,若业务需要真正的只读快照,元素类型也应使用不可变类型,例如字符串、不可变记录或创建了防御性副本的值对象。只把外层 List 换成 copyOf,不能自动保护嵌套的 Map、数组或自定义可变对象。
为什么 null 会让 List.copyOf 直接失败
List.copyOf 不接受空集合引用,也不接受含有 null 元素的集合。它在创建阶段就会拒绝这些输入,而不是生成一个以后再检查的特殊列表。这个行为对配置和权限列表很有用:空值问题会在边界处暴露,不会等到下游遍历时才产生含糊的错误。
import java.util.ArrayList;
import java.util.List;
public class NullInputDemo {
static List freeze(List input) {
if (input == null) {
return List.of(); // 业务若允许“没有配置”,先明确转成空列表
}
return List.copyOf(input); // 输入含 null 时仍会抛出 NullPointerException
}
public static void main(String[] args) {
var input = new ArrayList();
input.add("safe");
// input.add(null); // 不要把 null 静默混入需要冻结的配置
System.out.println(freeze(input));
}
}
是否把 null 当成空列表,应该由接口契约决定。上例的 freeze 只演示一种选择:方法参数本身为 null 时使用空列表;但集合内部出现 null 仍然保留失败,让调用方修复具体数据,而不是把缺失值悄悄变成字符串或默认权限。

把 List.copyOf 放在什么接口边界
一个实用位置是“接收输入后立即固化”。调用方可以继续修改自己的 ArrayList,而被调用方保存的是当时的顺序和元素引用。返回值则可以直接暴露为 List,不必让调用者知道内部究竟用了哪种不可修改实现。
import java.util.List;
final class QueryPlan {
private final List fields;
QueryPlan(List fields) {
this.fields = List.copyOf(fields); // 构造时固定列表结构,避免外部后续增删
}
List fields() {
return fields; // 返回不可修改列表,调用方不能反向改变计划结构
}
}
public class BoundaryDemo {
public static void main(String[] args) {
var fields = new java.util.ArrayList(List.of("id", "name"));
var plan = new QueryPlan(fields);
fields.add("created_at"); // 外部源集合变化不影响已构造的 plan
System.out.println(plan.fields()); // [id, name]
}
}
这个做法并不自动解决线程安全,也不代表元素内部没有共享引用。如果 fields 里放的是可变配置对象,仍然要在对象层做复制或改成不可变值类型。对普通字符串、枚举和不可变记录,List.copyOf 往往已经足够清晰。
选择 copyOf、unmodifiableList 还是可变副本
| 需求 | 更合适的写法 | 关键区别 |
|---|---|---|
| 创建结构不可变的列表副本 | List.copyOf(source) | 源集合后续结构变化不影响结果,拒绝 null 元素 |
| 只包一层只读视图 | Collections.unmodifiableList(source) | 源集合变化会反映到视图,仍共享同一个容器 |
| 需要继续增删改 | new ArrayList(source) | 获得可变容器,适合后续编辑 |
| 元素也要隔离 | 复制元素后再组装列表 | 需要按元素类型设计深拷贝或不可变值对象 |
最容易混淆的是 List.copyOf 与 Collections.unmodifiableList。前者创建自己的列表结构,后者只是给原列表加一层“不能通过这个引用修改”的视图。若原列表之后追加元素,视图可能看到新元素,而 copyOf 的结果不会跟着扩展。
使用前的四项检查
- 确认业务要的是“结构不可变副本”,而不是会随源列表变化的只读视图。
- 确认输入集合引用和每个元素都满足空值策略;需要允许
null时,先转换成业务明确的默认值。 - 确认元素本身是否可变;如果是嵌套集合、数组或可变 DTO,单层
copyOf不足以做深度快照。 - 确认所有修改操作都发生在复制前;副本创建后只把它当成读取接口使用。
两个容易混淆的问题
List.copyOf 会不会把源 ArrayList 的后续新增元素带过来?
不会。结果列表拥有独立的列表结构,创建完成后源集合再新增或移除元素,不会改变结果的元素数量和顺序。不过,若元素对象本身可变,结果中仍可能看到同一个对象的字段变化。
List.copyOf 和 Collections.unmodifiableList 哪个更安全?
要看边界。需要一次性的结构副本时选 List.copyOf;需要一个实时只读视图时才考虑 Collections.unmodifiableList。两者都不是深度复制,也都不能替代对可变元素和并发访问的设计。
-
479 收藏
-
337 收藏
-
128 收藏
-
149 收藏
-
202 收藏
-
339 收藏
-
205 收藏
-
208 收藏
-
247 收藏
-
322 收藏
-
454 收藏
-
133 收藏
-
481 收藏
-
152 收藏
-
476 收藏
-
493 收藏
-
115 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习