1. 概述
本文将系统梳理在 Java 中复制 List 的多种方式,并重点指出一个常见的“踩坑”点:看似复制成功,实则共享引用,导致修改一处影响多处。
如果你对 Java 集合框架还不熟悉,建议先阅读 Java 集合基础。
2. 使用构造函数复制
最简单直接的方式是使用 ArrayList
的构造函数,传入原始 List:
List<Plant> copy = new ArrayList<>(list);
✅ 优点:代码简洁,性能较好。
⚠️ 关键点:这种方式复制的是对象引用,而不是对象本身。也就是说,两个 List 中的元素指向的是堆中同一个对象。
这意味着:
❌ 如果你修改 copy
中某个 Plant
对象的属性,list
中对应对象也会被修改,因为它们是同一个实例。
因此,这种方式只推荐用于不可变对象(immutable objects),比如:
List<Integer> copy = new ArrayList<>(list);
Integer
是不可变类,一旦创建其值就不能更改。多个 List 共享同一个 Integer
引用是安全的,因为没人能改变它的值。
3. 避免 ConcurrentModificationException
⚠️ 常见问题:在多线程环境下,一边遍历(或复制)List,一边修改它,极易触发 ConcurrentModificationException
。
这通常发生在:一个线程正在复制 List,另一个线程同时在修改原 List。
解决方案:
✅ 使用线程安全的集合类
例如 CopyOnWriteArrayList
,它在每次修改时都会创建底层数组的新副本,读操作无需加锁,非常适合读多写少的场景。
详情参考:Java CopyOnWriteArrayList 详解
✅ 显式加锁
使用 ReentrantReadWriteLock
等同步工具,在复制期间对原集合加锁,保证读取时的一致性。
✅ 避免复制
如果可能,设计上避免在并发场景下频繁复制可变集合,考虑使用不可变集合或发布后不再修改的数据结构。
4. 使用 addAll 方法
另一种常见方式是先创建空 List,再调用 addAll
:
List<Integer> copy = new ArrayList<>();
copy.addAll(list);
⚠️ 注意:和构造函数一样,addAll
也是复制引用,不是深拷贝。
两个 List 仍然共享相同的对象引用,修改对象属性会相互影响。
5. 使用 Collections.copy()
Collections
工具类提供了一个 copy()
静态方法,用于将源 List 的元素复制到目标 List。
📌 关键限制:目标 List 的大小必须大于等于源 List,否则会抛出 IndexOutOfBoundsException
。
示例 1:目标 List 大小等于源 List
List<Integer> source = Arrays.asList(1, 2, 3);
List<Integer> dest = new ArrayList<>(Arrays.asList(4, 5, 6));
Collections.copy(dest, source);
// 结果:dest = [1, 2, 3]
所有元素被覆盖。
示例 2:目标 List 更大
List<Integer> source = Arrays.asList(1, 2, 3);
List<Integer> dest = new ArrayList<>(Arrays.asList(5, 6, 7, 8, 9, 10));
Collections.copy(dest, source);
// 结果:dest = [1, 2, 3, 8, 9, 10]
只有前三个元素被替换,后面的元素保留。
📌 注意:dest
必须是可变的 ArrayList
,不能是 Arrays.asList()
返回的固定大小列表,否则会抛出 UnsupportedOperationException
。
6. 使用 Java 8 Stream
Java 8 的 Stream API 提供了更灵活的复制方式,尤其适合需要过滤或转换的场景。
基本复制
List<String> copy = list.stream()
.collect(Collectors.toList());
带跳过(skip)
List<String> copy = list.stream()
.skip(1) // 跳过第一个元素
.collect(Collectors.toList());
带过滤(filter)
按字符串长度过滤:
List<String> copy = list.stream()
.filter(s -> s.length() > 10)
.collect(Collectors.toList());
按对象属性过滤:
List<Flower> flowers = list.stream()
.filter(f -> f.getPetals() > 6)
.collect(Collectors.toList());
空安全处理(Null-Safe)
为避免 list
为 null
导致 NullPointerException
,可以这样写:
List<Flower> flowers = Optional.ofNullable(list)
.map(List::stream)
.orElseGet(Stream::empty)
.collect(Collectors.toList());
✅ 推荐:这种写法简洁且安全,是处理可能为空集合的标准模式。
也可以组合使用,比如跳过第一个元素且空安全:
List<Flower> flowers = Optional.ofNullable(list)
.map(List::stream)
.orElseGet(Stream::empty)
.skip(1)
.collect(Collectors.toList());
7. 使用 Java 10 List.copyOf()
Java 10 引入了 List.copyOf()
,可以创建一个不可变的 List 副本:
List<T> copy = List.copyOf(list);
✅ 优点:
- 语法极其简洁。
- 返回的 List 是不可变的(immutable),防止意外修改。
- 如果传入的 List 本身不可变,
copyOf()
可能直接返回原引用,性能更优。
⚠️ 注意:返回的 List 不支持 add
、remove
等修改操作,调用会抛出 UnsupportedOperationException
。
8. 总结
方法 | 是否复制引用 | 是否支持过滤 | 是否线程安全 | 推荐场景 |
---|---|---|---|---|
new ArrayList<>(list) |
✅ 是 | ❌ 否 | ❌ 否 | 简单复制,尤其不可变对象 |
addAll() |
✅ 是 | ❌ 否 | ❌ 否 | 同上,语义更明确 |
Collections.copy() |
✅ 是 | ❌ 否 | ❌ 否 | 需保持索引位置,目标 List 已存在 |
Stream.collect() |
✅ 是 | ✅ 是 | ❌ 否 | 需要过滤、跳过、转换等操作 |
List.copyOf() (Java 10+) |
✅ 是 | ❌ 否 | ✅ 是(不可变) | 创建不可变副本,简洁安全 |
📌 核心要点:
- 所有上述方法都是浅拷贝(shallow copy),只复制引用。
- 如需深拷贝(deep copy),需手动实现或使用序列化等机制。
- 多线程环境下注意
ConcurrentModificationException
,优先考虑线程安全集合或不可变集合。 - Java 10 的
List.copyOf()
是创建不可变副本的首选方式,简单粗暴且安全。