Iterator 和 ListIterator 有什么区别?for-each 遍历时能删除元素吗?
简化版
Iterator(迭代器)是遍历集合的统一接口——所有集合(List/Set/Map 的 values 等)都能返回一个 Iterator,用 hasNext()/next() 遍历,屏蔽了底层结构差异(不管是数组还是链表,遍历方式一样)。ListIterator 是 Iterator 的增强版,只用于 List,多了几个能力:① 双向遍历(hasPrevious()/previous() 能往回走)、② 遍历时增删改(add()/set()/remove())、③ 获取索引(nextIndex()/previousIndex())。一个高频坑:for-each(增强 for)遍历时直接调集合的 remove() 会抛 ConcurrentModificationException——因为 for-each 底层是 Iterator,你绕过它直接改集合会触发 fail-fast;正确做法是用 迭代器自己的 remove()(或 removeIf)。
详细版
Iterator vs ListIterator:
| 能力 | Iterator | ListIterator |
|---|---|---|
| 适用集合 | 所有 Collection | 只有 List |
| 遍历方向 | 单向(只能往后) | 双向(往后 + 往前) |
| 遍历时删除 | remove()(删上一个 next 的) | remove() |
| 遍历时新增 | ❌ 不能 | ✅ add() |
| 遍历时修改 | ❌ 不能 | ✅ set()(改当前元素) |
| 获取索引 | ❌ 不能 | ✅ nextIndex()/previousIndex() |
// Iterator:统一遍历接口
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.isEmpty()) it.remove(); // ✓ 用迭代器自己的 remove(安全)
}
// ListIterator:双向 + 增删改(只用于 List)
ListIterator<String> lit = list.listIterator();
while (lit.hasNext()) {
String s = lit.next();
lit.set(s.toUpperCase()); // 修改当前元素
if (s.equals("x")) lit.add("y"); // 新增元素
}
// 往回遍历
while (lit.hasPrevious()) {
String s = lit.previous();
}
for-each 遍历删除的坑:
// ✗ 错误:for-each 里直接调集合的 remove → ConcurrentModificationException
for (String s : list) {
if (s.isEmpty()) list.remove(s); // 抛 CME!
}
// ✓ 正确1:用迭代器的 remove
Iterator<String> it = list.iterator();
while (it.hasNext()) { if (it.next().isEmpty()) it.remove(); }
// ✓ 正确2:removeIf(Java 8+,最简洁)
list.removeIf(String::isEmpty);
⚠️ for-each(增强 for)本质就是 Iterator 的语法糖——
for (String s : list)编译后是Iterator it = list.iterator(); while(it.hasNext()) { s = it.next(); }。所以在 for-each 里调集合的remove()(而非迭代器的 remove),会绕过迭代器直接改集合的modCount,迭代器下次next()检查发现 modCount 变了就抛ConcurrentModificationException(fail-fast 机制)。这是「for-each 里不能直接删元素」的原因。
完整版教学
一、Iterator:统一的遍历接口
Iterator 是「遍历集合的统一接口」——它的价值是「屏蔽不同集合的底层结构差异,提供一致的遍历方式」:
不同集合的底层结构不同:
ArrayList 是数组、LinkedList 是链表、HashSet 是哈希表...
如果每种集合都有自己的遍历方式,使用者要学一堆
Iterator 统一了遍历:
所有集合都实现 Iterable、能返回一个 Iterator
统一用 hasNext()(还有没有下一个)+ next()(取下一个)遍历
→ 不管底层是数组还是链表,遍历代码都一样
Iterator 是迭代器模式的应用——把「遍历」这个行为抽象成一个独立的对象(迭代器),使用者只依赖 hasNext/next 接口,不用关心集合内部怎么存的、怎么遍历的。这带来「遍历方式统一、集合和遍历解耦」的好处。for-each 循环就是基于 Iterator 的(语法糖)。所以任何 Iterable 的东西都能用 for-each。理解「Iterator 是统一遍历接口、屏蔽底层结构差异、是迭代器模式、for-each 基于它」,就理解了它的定位——它让「遍历」变得统一而解耦。
二、ListIterator:List 专用的增强迭代器
ListIterator 是 Iterator 的子接口、只用于 List,因为 List 是「有序、有索引」的,能支持更多迭代操作:
ListIterator 比 Iterator 多的能力(都是 List 的有序性带来的):
① 双向遍历:hasPrevious()/previous() —— 能往回走
(List 有顺序,所以能双向;Set 无序,往回走没意义,所以 Set 没有 ListIterator)
② 遍历时新增:add() —— 在当前位置插入元素
③ 遍历时修改:set() —— 修改刚遍历到的元素
④ 获取索引:nextIndex()/previousIndex() —— List 有索引才有这个
为什么只有 List 有 ListIterator?因为这些增强能力都依赖 List 的「有序 + 有索引」特性——双向遍历要有顺序、获取索引要有索引、指定位置增删要有位置概念,这些 Set(无序)都没有。所以 ListIterator 是「为 List 的有序性定制的增强迭代器」。它的典型用途:需要边遍历边增删改、或双向遍历 List 时用(普通 Iterator 只能删不能增改、只能单向)。理解「ListIterator 只用于 List、多了双向遍历/增删改/索引(依赖 List 的有序有索引)」,就掌握了它和 Iterator 的区别——它是 List 专用的「全能迭代器」。
三、核心坑:for-each 遍历删除抛 CME
这是集合遍历最高频的坑——for-each 里直接调集合的 remove() 会抛 ConcurrentModificationException(CME):
List<String> list = new ArrayList<>(List.of("a", "", "b"));
for (String s : list) {
if (s.isEmpty()) list.remove(s); // 遍历到 "" 时删除 → 抛 CME!
}
原因链条(要讲清楚):
① for-each 底层是 Iterator:
for (String s : list) 编译成 Iterator it = list.iterator(); while(it.hasNext()) s = it.next();
② 集合有一个 modCount(修改次数计数器):
每次结构性修改(add/remove)都会让 modCount +1
③ 迭代器创建时记录了当时的 modCount(叫 expectedModCount):
迭代器每次 next() 都检查:当前 modCount == expectedModCount 吗?
④ 你调 list.remove(s) → modCount +1,但迭代器的 expectedModCount 没变
→ 下次 it.next() 检查发现 modCount != expectedModCount → 抛 ConcurrentModificationException
核心:你「绕过迭代器」直接改集合(list.remove),破坏了迭代器对 modCount 的预期,迭代器检测到「集合在遍历期间被意外修改」就抛 CME(这就是 fail-fast 机制——快速失败,及早暴露并发修改问题)。理解「for-each 是 Iterator 语法糖、直接改集合破坏 modCount 预期触发 fail-fast CME」,就理解了这个坑的根源——不是不能删,是「不能绕过迭代器删」。
四、正确的遍历删除方式
既然「绕过迭代器删」会 CME,正确做法就是「通过迭代器删」或用专门的方法:
// 方式1:用迭代器自己的 remove(迭代器会同步更新 expectedModCount)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().isEmpty()) {
it.remove(); // ✓ 迭代器的 remove,会同步 modCount,不抛 CME
}
}
// 方式2:removeIf(Java 8+,最简洁推荐)
list.removeIf(String::isEmpty); // ✓ 内部用迭代器安全删除
// 方式3:倒序 for 循环(用索引,不用迭代器)
for (int i = list.size() - 1; i >= 0; i--) {
if (list.get(i).isEmpty()) list.remove(i); // ✓ 倒序删不会错位
}
// 方式4:收集到新集合(不改原集合)
List<String> result = list.stream().filter(s -> !s.isEmpty()).collect(toList());
为什么迭代器的 remove() 安全?因为迭代器的 remove 在删除后会「同步更新 expectedModCount = modCount」——它自己知道自己删了、更新了预期,所以下次 next 检查不会不一致。四种正确方式:① 迭代器 remove(经典);② removeIf(最简洁,Java 8+,推荐);③ 倒序索引 for 循环(不用迭代器、倒序避免删除后索引错位);④ Stream filter 收集到新集合(不改原集合)。最推荐 removeIf(一行、安全、清晰)。理解「用迭代器 remove(同步 modCount)/removeIf/倒序 for/Stream 都能安全删除」,就掌握了遍历删除的正确姿势。
五、fail-fast vs fail-safe
CME 是 fail-fast(快速失败) 机制的体现,与之相对的是 fail-safe(安全失败)(前面「fail-fast vs fail-safe」题详讲,这里作为迭代器的延伸):
fail-fast(快速失败):普通集合(ArrayList、HashMap)的迭代器
遍历时检测到集合被意外修改(modCount 变了)→ 立即抛 CME
目的:尽早暴露"遍历期间并发修改"的 bug(这通常是编程错误)
→ 它不是"并发安全机制",而是"bug 探测器"
fail-safe(安全失败):并发集合(CopyOnWriteArrayList、ConcurrentHashMap)的迭代器
遍历基于集合的"快照"或不检测修改 → 遍历期间改集合不抛异常
代价:可能读到旧数据(弱一致性)
关键认知:fail-fast 是「探测 bug」不是「保证安全」——它检测到「遍历期间集合被意外修改」就抛 CME,帮你尽早发现问题(无论是单线程里 for-each 删元素,还是多线程并发修改)。而并发集合用 fail-safe(不抛异常,但可能读到旧数据)。所以 CME 不代表「并发问题」,单线程 for-each 删元素也会 CME。理解「fail-fast 是 bug 探测器(modCount 检测)、fail-safe 是弱一致遍历、CME 单线程也会触发」,就把迭代器和 fail-fast 机制串起来了。
六、实践建议
关于迭代器和遍历的实践建议:
遍历删除元素:
优先用 removeIf(Java 8+,一行、安全、清晰)
其次用迭代器的 remove(经典写法)
绝不在 for-each 里直接调集合的 add/remove(会 CME)
遍历时需要增/改/双向:
用 ListIterator(能 add/set/双向),普通 Iterator 只能删
遍历修改的替代:
Stream + filter/map 生成新集合(函数式、不改原集合、更清晰)
并发遍历:
用并发集合(CopyOnWriteArrayList/ConcurrentHashMap,fail-safe 不抛 CME)
普通集合并发遍历+修改会 CME(甚至更严重的问题)
核心实践:① 遍历删除用 removeIf(首选)或迭代器 remove;② 别在 for-each 里直接改集合(CME);③ 需要增改双向用 ListIterator;④ 复杂的遍历转换用 Stream;⑤ 并发遍历用并发集合。这些实践的共同点是「别绕过迭代器改集合」——要么通过迭代器/removeIf 改、要么生成新集合、要么用并发集合。理解这些实践,就能避开遍历相关的所有坑。
记忆钩子:「Iterator 是统一遍历接口(迭代器模式、屏蔽底层结构、for-each 基于它)单向、只能 remove;ListIterator 只用于 List、多了双向遍历+add/set+索引(依赖 List 有序有索引);for-each 里直接调集合 remove 会抛 ConcurrentModificationException(fail-fast:for-each 是 Iterator 语法糖,绕过迭代器改 modCount 破坏预期);正确删除用 removeIf(首选)/迭代器 remove/倒序 for/Stream;fail-fast 是 bug 探测器(单线程也触发)、并发集合用 fail-safe」。
七、常见误区与追问
- 误区:Iterator 和 ListIterator 差不多。 ListIterator 是 Iterator 的增强、只用于 List,多了双向遍历、遍历时 add/set、获取索引(依赖 List 的有序有索引);Iterator 适用所有集合但只能单向 + remove。
- 误区:for-each 里可以直接 list.remove()。 会抛 ConcurrentModificationException——for-each 是 Iterator 语法糖,直接改集合破坏 modCount 预期触发 fail-fast;要用迭代器的 remove 或 removeIf。
- 误区:CME 是多线程并发问题。 单线程也会——for-each 里删元素(绕过迭代器改集合)就会触发;CME 是「遍历期间集合被意外修改」的探测(fail-fast),不限于并发。
- 误区:所有集合都有 ListIterator。 只有 List 有——ListIterator 的双向遍历、索引等依赖 List 的有序有索引,Set(无序)没有 ListIterator。
- 追问:for-each 遍历时怎么安全删除元素? 用迭代器的 remove(会同步 modCount)、或 removeIf(Java 8+,首选)、或倒序索引 for 循环、或 Stream filter 生成新集合;绝不在 for-each 里直接调集合的 remove。
- 追问:为什么迭代器的 remove 不抛 CME 而集合的 remove 会? 迭代器的 remove 删除后会同步更新 expectedModCount = modCount(自己知道自己删了),所以下次 next 检查一致;集合的 remove 只改 modCount、迭代器的 expectedModCount 没变,检查不一致就抛 CME。
- 追问:ListIterator 有哪些独有能力? 双向遍历(hasPrevious/previous)、遍历时新增(add)和修改(set)、获取索引(nextIndex/previousIndex)——都是普通 Iterator 没有的,依赖 List 的有序有索引。
八、加强记忆
Iterator 是遍历集合的统一接口(迭代器模式)——所有集合都能返回它,用 hasNext()/next() 遍历,屏蔽底层结构差异(数组/链表遍历方式一致),for-each 就是它的语法糖;它单向、只能 remove()。ListIterator 是 Iterator 的增强子接口、只用于 List,多了 ① 双向遍历(hasPrevious/previous)② 遍历时 add/set ③ 获取索引(nextIndex/previousIndex)——这些都依赖 List 的有序有索引(Set 无序所以没有 ListIterator)。最高频的坑:for-each 里直接调集合的 remove() 会抛 ConcurrentModificationException——因为 for-each 底层是 Iterator,你绕过迭代器直接改集合的 modCount,破坏了迭代器的 expectedModCount 预期,next() 检查不一致就触发 fail-fast 抛 CME(单线程也会触发,CME 是「遍历期间被意外修改」的 bug 探测器不是并发机制)。正确删除:removeIf(Java 8+ 首选)、迭代器自己的 remove(会同步 modCount)、倒序索引 for、Stream filter 新集合;并发遍历用并发集合(fail-safe 不抛 CME)。一句话「Iterator 统一遍历单向、ListIterator 只用于 List 能双向+增改+索引,for-each 里直接 remove 抛 CME(绕过迭代器破坏 modCount、fail-fast、单线程也触发),安全删除用 removeIf/迭代器 remove」。