遍历集合时删除元素抛异常?从 Iterator 源码讲起,彻底搞懂 fail-fast 机制 从 ConcurrentModificationException 到 fail-safe从 ListIterator 到自定义迭代器一篇带你真正掌握迭代器我们在日常开发中几乎天天都在遍历集合。但你有没有想过为什么在遍历ArrayList时用list.remove()会抛异常但用iterator.remove()却没事为什么ConcurrentHashMap的迭代器不会抛异常Iterator和ListIterator到底有什么区别这些问题的答案都藏在迭代器Iterator的设计哲学里。今天这篇文章我们从接口设计、快速失败fail-fast机制、双向遍历ListIterator、自定义实现到生产级避坑把Iterator彻底讲透。一、先上结论一张表看懂核心角色角色接口/类核心职责关键方法特点可迭代对象IterableT返回迭代器被遍历的对象iterator()增强for循环的“入场券”迭代器单向IteratorE单向遍历并安全删除hasNext()、next()、remove()最基础、最通用迭代器双向ListIteratorE双向遍历支持增/改hasPrevious()、previous()、add()、set()仅限List及其子类拆分迭代器SpliteratorE并行遍历Java 8trySplit()、forEachRemaining()Stream并行流的底层一句话记忆口诀Iterable→ 容器本身能产生迭代器。Iterator→ 单向箭头只能往前走能删。ListIterator→ 双向箭头能前能后能增能改。Spliterator→ 多箭头分块并行。二、Iterator基础为什么不用普通for循环1. 三种遍历方式对比ListStringlistnewArrayList(Arrays.asList(A,B,C));// 方式1普通 for 循环依赖下标for(inti0;ilist.size();i){System.out.println(list.get(i));}// 方式2增强 for 循环语法糖底层也是迭代器for(Strings:list){System.out.println(s);}// 方式3显式迭代器最灵活IteratorStringitlist.iterator();while(it.hasNext()){Stringsit.next();System.out.println(s);}2. 为什么需要迭代器解决了什么问题统一遍历接口无论List、Set、Queue遍历方式完全一致解耦数据结构。安全的删除操作普通for循环删除元素会导致索引错乱增强for循环直接删除会抛异常只有Iterator.remove()才是安全的。隐藏底层实现你不需要关心是数组还是链表只管hasNext()next()。三、核心方法详解hasNext、next、remove1.hasNext()和next()遍历的基本动作IteratorStringitlist.iterator();while(it.hasNext()){// 1. 问还有吗Stringsit.next();// 2. 拿拿到下一个并且游标后移System.out.println(s);}注意如果没有调用hasNext()直接调用next()当没有元素时会抛出NoSuchElementException。2.remove()唯一安全的删除方式这是迭代器最核心的实用价值。ListIntegernumbersnewArrayList(Arrays.asList(1,2,3,4,5));// ✅ 正确使用迭代器删除偶数IteratorIntegeritnumbers.iterator();while(it.hasNext()){Integernumit.next();if(num%20){it.remove();// 安全删除当前元素}}System.out.println(numbers);// [1, 3, 5]3. ⚠️remove()的两条铁律面试常问每次next()后只能调用一次remove()。必须先调用next()再调用remove()不能连续删除也不能在next()之前删除。IteratorIntegeritlist.iterator();while(it.hasNext()){// it.remove(); // ❌ 报错IllegalStateException没有先 nextIntegernumit.next();it.remove();// it.remove(); // ❌ 报错IllegalStateException连续删除两次}四、增强for循环的“甜蜜陷阱”重点语法糖for (String s : list)编译后就是while (it.hasNext()) { String s it.next(); ... }。致命错误在增强for循环中直接调用list.remove()。ListStringlistnewArrayList(Arrays.asList(A,B,C));// ❌ 错误抛出 ConcurrentModificationExceptionfor(Strings:list){if(B.equals(s)){list.remove(s);// 直接操作集合导致迭代器检测到 modCount 变化}}为什么报错因为增强for底层隐式创建了迭代器。集合内部有一个modCount计数器记录修改次数。list.remove()会修改modCount但迭代器内部的expectedModCount没有同步更新导致两者不相等迭代器检测到后立即抛出ConcurrentModificationException快速失败机制见下一节。✅ 正确做法使用显式Iterator调用it.remove()。五、ListIterator双向迭代器支持增、改ListIterator是Iterator的子接口专门为List提供支持双向遍历和元素修改/添加。1.ListIteratorvsIterator对比功能IteratorListIterator方向只能从前到后双向前/后删除✅remove()✅remove()添加❌ 不支持✅add(E e)修改❌ 不支持✅set(E e)获取索引❌ 不支持✅nextIndex()/previousIndex()适用范围Collection所有子类仅限List含ArrayList、LinkedList2. 使用示例双向遍历 添加ListStringlistnewArrayList(Arrays.asList(A,B,D));ListIteratorStringlitlist.listIterator();// 1. 从前往后遍历while(lit.hasNext()){Stringslit.next();if(B.equals(s)){lit.add(C);// 在 B 后面插入 C游标自动跳过新元素}}System.out.println(list);// [A, B, C, D]// 2. 从后往前遍历while(lit.hasPrevious()){Stringslit.previous();if(C.equals(s)){lit.set(X);// 修改当前元素为 X}}System.out.println(list);// [A, B, X, D]核心机制add()插入的元素位于当前游标之前即next()返回的元素之前。set()更新的是上一次next()或previous()返回的元素。六、ConcurrentModificationException快速失败fail-fast机制1. 什么是 fail-fastJava 集合ArrayList、HashMap等的迭代器在设计时采用了快速失败机制当迭代器被创建后如果集合内容被非迭代器方式如直接list.remove()修改迭代器会立即抛出ConcurrentModificationException而不是冒着风险继续执行。2. 底层原理源码视角// AbstractList 源码片段publicabstractclassAbstractListE{protectedtransientintmodCount0;// 集合结构修改次数// 迭代器内部privateclassItrimplementsIteratorE{intexpectedModCountmodCount;// 创建迭代器时保存的快照publicEnext(){checkForComodification();// 每次 next 都检查// ...}finalvoidcheckForComodification(){if(modCount!expectedModCount)thrownewConcurrentModificationException();}}}当list.remove()被调用时modCount但迭代器的expectedModCount没变。下次调用it.next()时发现两者不等直接抛异常。3. 如何正确避免单线程使用Iterator.remove()它会同步更新expectedModCount。多线程使用ConcurrentHashMap、CopyOnWriteArrayList等安全失败fail-safe集合或者加锁synchronized保护迭代代码块。七、安全失败fail-safe机制1. 什么是 fail-safe与 fail-fast 对应安全失败迭代器在遍历时不会抛出异常。因为它们操作的是原集合的克隆/快照修改原集合不会影响遍历中的快照。2. 典型代表ConcurrentHashMap的迭代器弱一致性CopyOnWriteArrayList的迭代器基于快照CopyOnWriteArrayListStringlistnewCopyOnWriteArrayList();list.add(A);list.add(B);for(Strings:list){// 底层是 CopyOnWrite 迭代器安全失败if(A.equals(s)){list.add(C);// ✅ 不会抛异常因为遍历的是快照}}System.out.println(list);// [A, B, C]新增元素不影响正在进行的遍历八、自定义迭代器实现面试实战如果让你设计一个支持迭代的自定义集合必须实现Iterable接口并重写iterator()方法。publicclassMyCollectionTimplementsIterableT{privateObject[]elementsnewObject[10];privateintsize0;publicvoidadd(Titem){elements[size]item;}// 1. 实现 Iterable 接口返回迭代器OverridepublicIteratorTiterator(){returnnewMyIterator();}// 2. 自定义迭代器内部类privateclassMyIteratorimplementsIteratorT{privateintcursor0;OverridepublicbooleanhasNext(){returncursorsize;}SuppressWarnings(unchecked)OverridepublicTnext(){if(!hasNext())thrownewNoSuchElementException();return(T)elements[cursor];}// 简化版本不支持 remove}publicstaticvoidmain(String[]args){MyCollectionStringmcnewMyCollection();mc.add(Java);mc.add(Iterator);for(Strings:mc){// 增强 for 循环生效System.out.println(s);}}}九、Java 8 新特性forEachRemaining()Iterator接口在 Java 8 中添加了一个默认方法forEachRemaining(Consumer? super E action)可以一次性消费剩余所有元素。ListStringlistArrays.asList(A,B,C,D);IteratorStringitlist.iterator();it.next();// 跳过 Ait.forEachRemaining(System.out::println);// 输出 B C D从当前位置到末尾十、思考题检验是否真的懂了// 问题 1下面代码会抛出什么异常为什么ListStringlistnewArrayList(Arrays.asList(A,B,C));for(Strings:list){if(B.equals(s)){list.remove(s);}}// 问题 2下面代码中ArrayList 和 CopyOnWriteArrayList 的表现有什么不同// 场景 AArrayListListIntegerlist1newArrayList(Arrays.asList(1,2,3));for(Integeri:list1){if(i2)list1.remove(i);}// 场景 BCopyOnWriteArrayListListIntegerlist2newCopyOnWriteArrayList(Arrays.asList(1,2,3));for(Integeri:list2){if(i2)list2.remove(i);}// 问题 3ListIterator 和 Iterator 都能删除元素为什么 ListIterator 还能添加元素答案选中下方空白区域查看抛出ConcurrentModificationException。增强for底层是迭代器list.remove()修改了modCount但迭代器内部的expectedModCount没变导致冲突。场景 A抛出ConcurrentModificationExceptionfail-fast场景 B正常运行因为CopyOnWriteArrayList是 fail-safe 机制遍历的是快照修改原集合不影响遍历。ListIterator是专门为List设计的因为List有索引结构add()方法可以精确控制插入位置通过游标。而Set是无序的不支持add()到指定位置所以普通的Iterator不提供add()以保证所有集合的一致性和安全性。总结终极速查表知识点一句话记忆Iterator核心方法hasNext()问路next()走路remove()回头清除Iterable实现这个接口才能用增强for循环增强for底层是迭代器直接list.remove()会翻车ListIterator双向走还能增删改但只能用在List上fail-fast快速失败ArrayList、HashMap默认检测到并发修改立即抛异常fail-safe安全失败CopyOnWriteArrayList、ConcurrentHashMap遍历快照安全但不保证实时一致性forEachRemainingJava 8 提供一次性消费剩余所有元素 互动话题你在实际开发中有没有因为遍历时删除元素导致ConcurrentModificationException的“血泪史”或者用过CopyOnWriteArrayList踩过性能的坑欢迎评论区分享如果觉得有收获别忘了点赞、收藏、转发让更多 Javaer 彻底搞懂迭代器的底层原理我们下篇见发布日期2026-08-30

相关新闻

最新新闻

基于ESP32的智能原汁机控制原型:NFC配方识别与堵转保护设计

基于ESP32的智能原汁机控制原型:NFC配方识别与堵转保护设计

美的原汁机这条产品标题里最有信息量的词,不是“果汁自由”,也不是“NFC”,而是很多人会忽略的“整果免切直投”。把苹果、雪梨、胡萝卜整个投进进料口,按下开关就得到一杯无添加果汁,背后并不是把榨汁机做“大”那么简…

2026/8/31 6:49:43
小鹏机器人融资9亿美元,人形机器人赛道的性感与危险

小鹏机器人融资9亿美元,人形机器人赛道的性感与危险

小鹏机器人首轮融资超9亿美元的消息传出来后,行业群里讨论最多的不是金额本身,而是两个词:性感、危险。我的判断是,如果这个金额最终得到官方确认,它意味着人形机器人赛道正式从技术展示期进入资本大额下注期。这里说的…

2026/8/31 6:49:43
小红书2020校招算法笔试题解析:从KMP到Dijkstra的考点与实战复盘

小红书2020校招算法笔试题解析:从KMP到Dijkstra的考点与实战复盘

小红书2020校招算法笔试题卷一,算是当年那批互联网校招卷子里比较有代表性的一套。我当时刷完最深的感受是:它没有刻意追求偏题怪题,而是在基础算法、数据结构、机器学习理论之间找了一个相对平衡的点。无论你投的是算法岗还是推荐算法岗&…

2026/8/31 6:49:43
LLVM MIR改用block arguments替代phi:代码生成的新变革

LLVM MIR改用block arguments替代phi:代码生成的新变革

最近在阅读 LLVM Code Generation 相关的 RFC 时,有一个标题特别吸引我:Change MIR to use block arguments instead of phis。作为一个经常和 MIR、SelectionDAG 打交道的后端开发者,我对这种“表面上是语法改动,实际会牵动整个代…

2026/8/31 6:49:43
从75配列到OLED屏:ROG夜魔三模机械键盘全面拆解

从75配列到OLED屏:ROG夜魔三模机械键盘全面拆解

最近不少朋友在挑机械键盘时,都被 75 配列、Gasket 结构、无线三模、OLED 屏这一串关键词绕晕了。一方面想要桌搭好看、手感软弹,另一方面又担心无线延迟影响游戏发挥,还怕 OLED 屏只是噱头。本文就用 ROG 夜魔这款关注度很高的 75% 三模键盘…

2026/8/31 6:49:43
Codex辅助UVM学习:从核心概念到搭建最小验证环境

Codex辅助UVM学习:从核心概念到搭建最小验证环境

在验证领域从 SystemVerilog 逐步过渡到 UVM 的过程中,很多人都会被组件划分、phase 机制、config_db 传参这些抽象概念绕晕。网上资料不少,但要么是官方手册的翻译腔,要么是一堆代码片段缺少上下文。最近我在用 Codex 辅助学习和搭建 UVM 验…

2026/8/31 6:44:43