登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

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 改掉调用方的数据。

Java List.copyOf 从可变源集合创建不可变列表副本的静态结构图
图1:展示源集合、List.copyOf 与不可变列表之间的结构关系;这是静态说明图,不是运行截图。

副本不能改,元素对象却不一定不可变

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 仍然保留失败,让调用方修复具体数据,而不是把缺失值悄悄变成字符串或默认权限。

Java List.copyOf 的结构不可变与元素可变边界静态关系图
图2:对比列表结构、源集合和元素对象三层边界,说明 copyOf 不等于深度复制;这是静态结构图,不是执行证据。

把 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。两者都不是深度复制,也都不能替代对可变元素和并发访问的设计。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>