news 2026/9/3 6:57:52

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
遍历时删元素为什么报错:modCount 这个“计数器“在背后盯着你

「Java 进阶之路」系列 Day25

写在前面

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


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

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


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

List<Integer>list=newArrayList<>(List.of(1,2,3,4,5));for(Integeri:list){if(i==3){list.remove(i);// 直接调用集合自身的remove方法}}// 抛出ConcurrentModificationException

创建Iterator时记录expectedModCount等于当前modCount

遍历过程中调用集合自身的remove方法

集合内部modCount自增

Iterator完全不知道modCount已经变了

下一次调用next时检查modCount是否还等于expectedModCount

不相等 抛出ConcurrentModificationException

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

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

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

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


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

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

List<Integer>list=newArrayList<>(List.of(1,2,3,4,5));for(inti=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对象,这是模块三集合框架的收官篇。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 6:57:50

List<String> 和 List<Integer>,运行时其实是同一个 Class

「Java 进阶之路」系列 Day26写在前面 模块三集合框架写到这篇收官&#xff0c;回过头讲讲一直陪着这些集合类出现、却很少被单独拎出来讲透的泛型。很多人知道"Java泛型是伪泛型"这个说法&#xff0c;但问一句"伪在哪、具体擦除掉了什么"&#xff0c;就说…

作者头像 李华
网站建设 2026/9/3 6:57:00

MiMo-V2.5 多模态内容生成实战指南

在内容创作领域,我们常常面临一个共同的痛点:需求量大、场景复杂,但人力和时间却极其有限。无论是电商大促期间需要海量短视频素材,还是教育行业要为不同学生定制专属习题,传统的手工制作模式往往显得力不从心。很多团队不得不陷入“加班赶工 - 质量下降 - 反复修改”的恶…

作者头像 李华
网站建设 2026/9/3 6:55:54

基于YOLOv8的水质污染监测系统:从算法到完整应用的工程实践

简介&#xff1a;本资源是一套基于YOLOv8实现的水质污染目标检测系统&#xff0c;面向计算机、人工智能、自动化等专业的本科生及初学者&#xff0c;解决水体中典型污染物&#xff08;如油膜、藻类、悬浮物、垃圾、泡沫&#xff09;的自动识别与可视化监测问题&#xff0c;特别…

作者头像 李华
网站建设 2026/9/3 6:53:03

【需求设计】小写数字金额转换成中文大写金额

amount2cn 设计文档 小写数字金额 → 中文大写金额转换. 原文参考:https://blog.csdn.net/liudglink/article/details/106820490本文件为设计说明,代码实现请访问 gitee仓库 [零]用法与整体流水线 多文件程序(同属 package main),无外部依赖,由 go.mod 管理,直接运行: go r…

作者头像 李华
网站建设 2026/9/3 6:52:37

双曲线方程全解析:从定义推导到高考解题技巧

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 6:50:17

Linux之socket编程(四)----Tcp

Tcp tcp进行网络通信在很多部分和udp通信时很相似的 大纲 init 初始化和udp很相似 就是socket的第二个参数时sock_stream 除了上面这些 litsen Tcp依然还需要litsen接口 监听就是有点像接电话,有人打进来电话你就要接 start accept accept的返回值是有些不同的,相对于udp…

作者头像 李华