真正覆盖java集合迭代异常的关键是围绕concurrentmodificationexception触发条件设计可复现测试:覆盖单线程三类cme路径、多线程竞态场景及安全替代方案的正确性,而非简单try-catch。

要真正覆盖 Java 集合迭代中的异常场景,关键不是“多写几个 try-catch”,而是围绕 ConcurrentModificationException(CME)的触发条件设计可验证、可复现的测试路径。它本质是 fail-fast 机制的运行时反馈,测试目标应是:让校验逻辑必走、让 modCount 与 expectedModCount 必不等、让异常必抛出且被观测到。
覆盖单线程下 CME 的三种典型触发路径
这是最易忽略也最常考的测试维度。必须分别构造以下三类 case,并断言抛出 ConcurrentModificationException:
-
增强 for 循环中调用集合 remove():例如
for (String s : list) { if ("x".equals(s)) list.remove(s); }—— 此写法在第一次 next() 后、第二次 hasNext() 前触发校验失败 -
Iterator 遍历中调用集合 add()/remove()(非 iterator.remove()):例如
Iterator it = list.iterator(); while(it.hasNext()) { it.next(); list.add("new"); } -
ListIterator 遍历时用 list.set() 以外的方式修改结构:如正向遍历中调用
list.remove(0),哪怕没动当前元素位置,modCount 也会变
覆盖多线程竞争下的 CME 与数据错乱风险
单线程测试无法暴露并发真实问题。需用 ExecutorService 或 CountDownLatch 控制竞态窗口:
- 启动两个线程:一个持续调用
iterator().hasNext()+next(),另一个在固定时机调用list.add()或list.clear() - 运行 100 次以上,观察是否稳定抛出 CME(注意:fail-fast 不保证 100% 检测,但高概率复现即可证明机制生效)
- 补充断言:若未抛异常,检查最终 list.size() 是否与预期不符——这说明发生了 silent corruption,比 CME 更危险
覆盖安全替代方案的“不抛异常”行为
只测“抛异常”不够,还要测“不该抛却没抛”的正确性。每种规避写法都需对应一个正向测试:
- 用
iterator.remove()遍历时删除元素 → 断言最终 list 不含目标值,且无异常 - 用
list.removeIf(predicate)→ 断言返回 true,且 list 元素数减少,size 准确 - 用
CopyOnWriteArrayList多线程读写 → 断言遍历线程看到的是快照,不抛 CME,且写线程修改对本次遍历不可见
不推荐用 try-catch 包裹来“覆盖异常分支”
JaCoCo 等工具会把 catch (ConcurrentModificationException e) { } 算作已覆盖,但这毫无工程价值:
- CME 是逻辑错误信号,不是业务可恢复异常,捕获后继续执行往往导致状态不一致
- checkForComodification() 方法内的 if 判断为 true 并进入 throw 分支
- @Test(expected = ConcurrentModificationException.class) 或
assertThrows显式验证,而非靠空 catch “凑数”
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











