← 返回题目列表

Iterator 和 ListIterator 有什么区别?for-each 遍历时能删除元素吗?

中等 第 23 / 30 题 更新于 2026/07/27
IteratorListIterator迭代器fail-fast

简化版

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

能力IteratorListIterator
适用集合所有 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 专用的增强迭代器

ListIteratorIterator 的子接口、只用于 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」。