Java 修改 subList 后原列表也变了,继续迭代时报并发修改异常
辨认 subList 区间视图与独立副本,按业务选择复制片段或通过活动 ListIterator 修改,保留浅拷贝和失效视图边界。
从 Java 列表取出一段 subList,给这段列表新增元素后,原列表也变了;原先已经创建的迭代器继续读取时,还可能抛出 ConcurrentModificationException。即使整个过程只有一个线程,也会发生这种情况。
subList 返回原列表的区间视图,不是独立副本。通过可修改的视图增删元素,会影响原列表的结构;原列表已有的迭代器并不知道这次修改来自“另一变量”。
下面限定普通 ArrayList 或 LinkedList,示例采用 Java 7 可用语法。并发集合、自定义 List 及不允许增删的列表有不同约定,不能只凭变量类型是 List 就套用。
确认修改的是视图还是副本
以下结构会修改原列表,并使此前创建的迭代器面临结构变更:
List<Integer> original =
new ArrayList<Integer>(Arrays.asList(1, 4, 5));
Iterator<Integer> iterator = original.iterator();
iterator.next();
List<Integer> part = original.subList(0, 2);
part.add(1, 2);
iterator.next(); // 可能抛出 ConcurrentModificationException
这里的片段需导入 java.util 下相应类型。区间 0, 2 包含下标 0,不包含下标 2。part.add 后原列表已不再是 [1, 4, 5];异常发生在继续迭代的位置,不代表那里才是修改发生的位置。
排查时追溯列表的创建与传递,找出 subList、原列表以及已有迭代器的生命周期。仅重命名 part、增加一个 List 变量或换成 listIterator(),都不会让视图变成独立数据。
需要独立编辑片段时,创建新列表
如果目标是修改片段,同时让原列表及其遍历内容保持不变,可以在修改前复制区间:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
public class SubListCopyExample {
public static void main(String[] args) {
List<Integer> original =
new ArrayList<Integer>(Arrays.asList(1, 4, 5));
Iterator<Integer> iterator = original.iterator();
System.out.println(iterator.next());
List<Integer> part =
new ArrayList<Integer>(original.subList(0, 2));
part.add(1, 2);
part.add(2, 3);
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
System.out.println(original);
System.out.println(part);
}
}
遍历应仍依次读取原列表的 1、4、5;最后两个列表分别为 [1, 4, 5] 和 [1, 2, 3, 4]。新列表的结构独立于原列表,但复制的是元素引用,不是对业务对象深拷贝。若元素可变,修改元素内部字段仍可能被两边看到。
副本应在原视图仍有效时创建。若原列表已经在视图之外发生结构修改,不能指望再包一层 new ArrayList<>(part) 就能挽救失效视图;应回到可靠的原始列表与边界重新取值。
业务本来就要修改原列表时
如果要求把新元素插入原列表,并继续同一次遍历,改成副本会改变业务结果。对于支持该操作的列表,可以通过正在使用的 ListIterator 插入:
ListIterator<Integer> iterator = original.listIterator();
iterator.next();
iterator.add(2);
iterator.add(3);
这段需导入 java.util.ListIterator,并从原始 [1, 4, 5] 列表开始。插入位置在迭代器游标之前;随后 next() 仍读到 4,不会先读到刚插入的 2、3。不要通过强制类型转换把任意 iterator() 的返回值当成 ListIterator。
若修改函数只能拿到片段,拿不到正在遍历的迭代器,需要调整修改时机或接口,让“独立编辑”与“修改原列表”有明确归属。不能在外层仍有迭代器活动时,任由内部方法通过视图改变原列表,再把异常吞掉。
反方向也需要注意:取得 subList 后,直接对原列表做结构修改,视图的后续行为会变得未定义。不要依赖每次都在同一个方法抛出异常,也不要靠“这一次没报错”证明操作有效。
核对数据关系再提交修复
验收时同时观察原列表、片段和遍历顺序,并覆盖空区间、完整区间、非法边界及可变元素。快速失败异常只是帮助发现错误的机制,不是线程安全保证,也不应作为程序正常控制流程。
保留原来的修改流程与输入样例,确认调用方确实需要副本后再替换。若旧流程已经改动原列表,仅回退代码不会恢复元素;需要根据此前保留的数据恢复,浅拷贝也无法撤销元素内部字段的变化。


