增强 for(for-each)在修改集合时到底能不能用?

增强 for(for-each)在修改集合时到底能不能用?

增强for循环为什么会 ConcurrentModificationException (CME)、以及正确改法讲清楚。看完你能判断:什么时候能改、怎么改才安全、什么时候会炸。


1)增强 for 是什么?——编译器“反糖”成迭代器

你写的:

for (E e : list) {
    body(e);
}

编译后等价于(核心):

for (Iterator<E> it = list.iterator(); it.hasNext();) {
    E e = it.next();
    body(e);
}

结论:增强 for 一定使用 Iterator 来遍历。这也是触发 fail-fast 的根源。


2)为什么遍历时“直接改 List”会 CME?

ArrayList 为例,容器里有个结构性修改计数 modCount;迭代器在创建时把它拷贝为 expectedModCount。每次 next() / hasNext() 都会做:

if (modCount != expectedModCount)
    throw new ConcurrentModificationException();

当你在增强 for 里绕过迭代器直接改集合(list.add/remove/clear)时,调用的是list的add/remove/clear而不是迭代器的add/remove/clear所以会发生如下情况:

  • 容器的 modCount 变了,

  • 迭代器手里的 expectedModCount 没变,

  • 下一次 hasNext()/next()CME

典型“会炸”的代码

List<Integer> list = new ArrayList<>(List.of(1,2,3));
for (Integer x : list) {
    if (x == 2) list.remove(x);   // 结构性修改:modCount++,迭代器不知情 → 下一步CME
}

注:x == 2 是引用比较,整数缓存区间(-128~127)里“看着像对”,更稳妥写法是 Integer.valueOf(2).equals(x)

时序图

以下是两张高兼容 Mermaid 图,完整呈现“增强 for → 反糖 → CME 检测”的流程:

  • 图 1:时序图(含两条分支:直接改 List → 抛 CME;用迭代器改 → 安全)

  • 图 2:决策流(一眼看懂什么时候会 CME)


图 1|增强 for → 反糖 → 迭代检查(含两种修改分支)

sequenceDiagram autonumber actor Client participant Compiler participant List as ArrayList participant It as Iterator Client->>Compiler: for (E e : list) { ... } Compiler-->>Client: desugar to Iterator loop Client->>List: iterator() List-->>Client: return Iterator Client->>It: hasNext() It-->>Client: true or false Client->>It: next() It->>It: checkForComodification (modCount vs expectedModCount) alt client modifies list directly Client->>List: remove/add/clear List->>List: structural change, modCount increases Client->>It: next() or hasNext() It->>It: checkForComodification It-->>Client: throw ConcurrentModificationException else client uses iterator to modify Client->>It: remove() or set() or add() via ListIterator It->>List: structural change if needed List-->>It: modCount updated It->>It: expectedModCount set to current modCount Client->>It: next() It-->>Client: continue normally end

图 2|修改方式 → 是否 CME 的决策流

flowchart TD A[For each start, desugared to Iterator] --> B{How to modify} B --> C[List add or remove or clear] C --> D[Next iterator check] D --> E{modCount equals expectedModCount} E --> F[CME thrown] E --> G[Continue] B --> H[Use iterator remove] H --> G B --> I[Only set value, no size change] I --> G

关键点一览

  • 反糖:增强 for 一定变成 Iterator 循环(list.iterator()hasNext()next())。

  • fail-fast:迭代器在 next()/hasNext() 前做 checkForComodification,比较 modCountexpectedModCount

  • 会 CME 的情况:遍历中绕过迭代器结构性修改list.add/remove/clear 等)→ modCount 变了,迭代器快照没变 → 抛 ConcurrentModificationException

  • 安全修改:用迭代器自己的方法iterator.remove(),或 ListIterator.add/set/remove),这些方法会在成功后把 expectedModCount 同步到最新。

  • 仅改值list.set(i, v) 不改变结构,通常不触发 CME,但要注意你的业务语义是否合理。


3)那为什么“索引 for”通常不报错?

for (int i = 0; i < list.size(); i++) {
    if (Integer.valueOf(2).equals(list.get(i))) list.add(90);
}
  • 这里 没用迭代器,自然也没有 fail-fast 检查,所以一般不会抛 CME

  • 但逻辑风险很大:size() 在变,可能 越跑越长、跳元素或重复处理。删除时尤其容易“跳过”元素(因为左移)。


4)遍历时“可以改”的正确方式

✅ 用迭代器自己的方法(推荐)

  • Iterator.remove():删除刚由 next() 返回的那个元素

  • ListIterator.add(E):在当前位置插入

  • ListIterator.set(E):替换最近返回的元素(非结构性修改)

它们会在内部同步 expectedModCount不触发 CME

ListIterator<Integer> it = list.listIterator();
while (it.hasNext()) {
    Integer x = it.next();
    if (Integer.valueOf(2).equals(x)) {
        it.remove();       // 或 it.set(99) / it.add(90)
    }
}

✅ 只改“值”不改结构

for (int i = 0; i < list.size(); i++) {
    list.set(i, transform(list.get(i)));  // 不改大小,通常安全
}

✅ 倒序删除(索引 for)

for (int i = list.size() - 1; i >= 0; i--) {
    if (needRemove(list.get(i))) list.remove(i); // 不会跳元素
}

5)增强 for 的“安全与不安全”一览

操作 在增强 for 中
it.remove() / listIterator.add() / listIterator.set() ✅ 安全(同步快照)
list.set(i, v)(仅改值) ✅ 一般安全(不改结构)
list.add(e) / list.remove(i) / list.clear() ❌ CME(结构性修改)
其他线程并发修改同一个 ArrayList ❌ CME(或更糟)

6)顺带:Map 的增强 for 也一样

遍历 map.entrySet() 同理,增强 for 底层用迭代器:

for (Iterator<Map.Entry<K,V>> it = map.entrySet().iterator(); it.hasNext();) {
    Map.Entry<K,V> e = it.next();
    if (needRemove(e)) it.remove();   // 用迭代器 remove
}

直接 map.remove(key)CME


7)并发场景的替代方案

  • CopyOnWriteArrayList:读多写少、每次写复制数组;迭代器弱一致,不会 CME,但写入代价高。

  • 外部同步Collections.synchronizedList(new ArrayList<>()) 或显式锁,确保“遍历与修改”互斥。

  • 批处理:先收集要改的索引/元素,遍历结束后统一修改。


8)实战清单(记住就够用)

  • 增强 for 一定是迭代器 → 结构性修改请用 迭代器自带方法

  • 只改值(不改大小)一般没事,但要保证逻辑正确。

  • 索引 for 不会 CME,但要防 边界变化/元素搬移 的逻辑坑。

  • 并发就别用 ArrayList 直接改,考虑并发容器或外部同步。

  • 装箱类型比较用 equals,不要依赖 == 的缓存巧合。

posted on 2025-11-12 19:49  滚动的蛋  阅读(13)  评论(0)    收藏  举报

导航