遍历时删元素为什么报错:modCount 这个"计数器"在背后盯着你

「Java 进阶之路」系列 Day25

写在前面

"遍历集合的时候不能直接调用集合的remove方法",这条经验大家都知道,但问一句"为什么会报错、报的又是什么异常",很多人只记得一个笼统的印象。这篇把Iteratorfail-fast机制背后的modCount计数器讲清楚,顺便说说这套机制到底能不能完全依赖。


一、是什么:fail-fast 是一种"尽早报错"的检测机制

ArrayListHashMap这些集合内部都维护着一个modCount字段,记录这个集合被"结构性修改"(增加或删除元素,单纯改值不算)过多少次。Iterator在创建的那一刻,会把当时的modCount记下来,存成自己的expectedModCount;之后每调用一次next(),都会检查一下"当前的modCount是不是还等于我当初记下来的那个值",不相等就说明集合在遍历过程中被别的地方动过手脚,直接抛出ConcurrentModificationException------这就是fail-fast(快速失败)的含义:与其让程序继续在一个被意外修改过的集合上跑下去、产生更难排查的诡异结果,不如第一时间报错,让问题尽早暴露。


二、为什么会报错:一次典型的踩坑复现

java 复制代码
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));

for (Integer i : list) {
    if (i == 3) {
        list.remove(i);   // 直接调用集合自身的remove方法
    }
}
// 抛出ConcurrentModificationException
flowchart TB A[创建Iterator时记录expectedModCount等于当前modCount] --> B[遍历过程中调用集合自身的remove方法] B --> C[集合内部modCount自增] B --> D[Iterator完全不知道modCount已经变了] D --> E[下一次调用next时检查modCount是否还等于expectedModCount] E --> F[不相等 抛出ConcurrentModificationException]

问题就出在list.remove(i)这一步for-each语法糖底层就是用Iterator实现的,但list.remove(i)调用的是ArrayList自己的remove方法,不是Iteratorremove方法。ArrayListremove执行完之后,modCount自增了,但Iterator手里那份expectedModCount完全不知情、还停留在旧值------下一次Iterator调用next()推进遍历时,一检查发现两者对不上,立刻抛出异常。

正确的做法是用Iterator自己的remove()方法

java 复制代码
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
    Integer i = it.next();
    if (i == 3) {
        it.remove();   // 用Iterator自己的remove方法
    }
}

Iterator.remove()内部在删除元素的同时,会同步把expectedModCount也更新成最新的modCount ,两者始终保持一致,自然不会触发检测。这也是为什么"遍历删除要用Iteratorremove"这条经验背后真正的原理------不是Iterator有什么特殊权限,而是它顺手帮你把这两个计数器的值重新对齐了。


三、怎么用:两个容易被混淆的相关陷阱

陷阱一:用普通下标 for 循环删除元素,不报错但结果不对

java 复制代码
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
for (int i = 0; i < list.size(); i++) {
    if (list.get(i) == 3) {
        list.remove(i);   // 不会抛异常,但结果可能不符合预期
    }
}

这种写法不会ConcurrentModificationException(因为压根没用Iterator),但如果删除的元素后面还有连续需要删除的元素,会因为"删除后所有元素前移、下标却继续往后走"而漏检查一个元素------这和fail-fast是两个完全不同的问题,只是经常被放在一起讨论,写代码时要分清楚:Iterator遍历删除,报的是fail-fast异常;用下标遍历删除,出的是逻辑漏删的bug,两者的成因完全不一样。

陷阱二:fail-fast 只是"尽力检测",不是100%保证

JDK官方文档里对这套机制有一句关键说明:modCount的检查本身不是同步的 ,多线程场景下即使检测机制生效了,也不能保证一定能捕获到并发修改;反过来,某些边界情况下即使真的发生了并发修改,也可能因为时机凑巧而没有触发检测、蒙混过关。fail-fast只是一种尽最大努力去暴露问题的调试辅助手段,不能把它当成一种可以依赖的并发控制机制去设计代码 ------真正需要多线程安全遍历的场景,应该用Day08讲过的CopyOnWriteArrayList(遍历用的是创建时的数组快照,天生不会抛这个异常,属于"fail-safe"设计)或者ConcurrentHashMap这类专门为并发设计的容器。


四、面试追问

Q1:什么是 fail-fast 机制,它是怎么实现的?

fail-fast是集合类在遍历过程中检测到被意外结构性修改时,第一时间抛出ConcurrentModificationException的一种保护机制。实现原理是集合内部维护一个modCount字段记录被修改的次数,Iterator创建时记下当时的modCount作为expectedModCount,之后每次调用next()都会比较两者是否相等,不相等就说明遍历期间集合结构被改动过,直接抛出异常。

Q2:为什么在 for-each 循环里直接调用集合的 remove 方法会抛异常,用 Iterator 的 remove 方法就不会?

for-each底层是用Iterator实现遍历的,但直接调用集合自身的remove方法,只会让集合内部的modCount自增,Iterator手里记录的expectedModCount并不知情,两者变得不一致,下次调用next()检测到不一致就会抛异常。Iterator自己的remove()方法在删除元素的同时会同步更新expectedModCount,让两者始终保持一致,所以不会触发这个检测。

Q3:用普通下标 for 循环遍历删除元素,会抛 ConcurrentModificationException 吗?

不会,因为这种写法完全没有用到Iteratorfail-fast检测机制根本没有介入。但这种写法有另一个问题:删除元素后集合会整体前移,而下标却继续往后走,如果有连续多个需要删除的元素,会因为下标错位漏掉其中一部分该删除的元素,这是一个逻辑正确性问题,和fail-fast异常是两回事。

Q4:fail-fast 机制能不能保证百分之百检测到并发修改?

不能。JDK官方文档明确说明modCount的检查本身不是同步的,多线程环境下既可能因为时机原因没有检测到真实发生的并发修改,也不能保证在所有场景下都表现一致。fail-fast只是一种尽力而为的调试辅助手段,不能当作可靠的并发控制机制来设计代码逻辑。

Q5:如果需要在多线程环境下安全地遍历一个集合,应该怎么做?

不应该依赖普通集合的fail-fast机制,而应该使用专门为并发场景设计的容器,比如CopyOnWriteArrayList------它的迭代器遍历的是创建时刻的数组快照,遍历期间即使有其他线程在修改原集合也不会抛异常(fail-safe设计);或者使用ConcurrentHashMap这类内部有精细同步机制的并发容器,具体选择取决于业务场景的读写比例和一致性要求。


下一篇预告

Day26 讲泛型的本质------类型擦除到底擦掉了什么,为什么List<String>List<Integer>在运行时其实是同一个Class对象,这是模块三集合框架的收官篇。

相关推荐
苍何1 小时前
原来世界模型,已经能边玩边生成了
后端
七牛开发者1 小时前
拆解 DeepSeek Harness:Profile 与 Bundle 如何装配运行时
前端·javascript·后端
HjhIron1 小时前
从零搭建后端业务:数据库表设计、索引优化与部署架构解析
后端
Zane19941 小时前
为什么 del 一个对象有时立刻消失,有时却纹丝不动?一文讲透引用计数与垃圾回收
后端·python
编码浪子1 小时前
Rust 智能指针深度实战:Box/Rc/Arc/RefCell 选型决策与内存管理避坑
开发语言·后端·rust
电商API_180079052471 小时前
速卖通商品采集API技术文章
java·开发语言·c++·api·跨境电商·商品详情
郑州光合科技余经理2 小时前
海外版外卖系统架构:订单怎么流转、权限怎么分
java·开发语言·前端·系统架构·uni-app·php·ai编程
提线木偶2 小时前
CORS 到底谁说了算?一份跨域配置的避坑指南
前端·后端
一木 之林2 小时前
五、C++新特性、关键字与编译原理
java·jvm·算法