迭代器 Iterator 与 Iterable
本教程共 100 篇 · 第 81 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
81. 迭代器 Iterator 与 Iterable
本节目标:分清 Iterable(能不能被 for-each 遍历)和 Iterator(遍历时的游标),理解 fail-fast 机制,学完知道遍历中安全删元素的唯一正确姿势。
为什么需要迭代器
前面遍历集合都用增强 for(for (X x : list)),写起来很爽。但你有没有想过,for-each 背后到底是谁在干活?答案就是迭代器。任何能被 for-each 遍历的对象,都实现了 Iterable 接口。
public interface Iterable<T> {
Iterator<T> iterator(); // 返回一个迭代器
}
```bash
只要一个类实现了 `Iterable`,就能用 for-each。集合框架里 List、Set、Queue 全都实现了它,所以都能直接遍历。
## Iterator 是遍历的游标
`Iterator` 是真正负责一步步走动的「游标」,三个方法最关键:
```java
Iterator<String> it = list.iterator();
while (it.hasNext()) { // 还有下一个吗
String s = it.next(); // 取下一个
System.out.println(s);
}
hasNext():后面还有没有元素。next():取出下一个元素,并把游标往后挪一格。remove():删除刚被next()取出的那个元素。
Note调用
next()之前通常先hasNext()判断,否则没元素了还调next()会抛NoSuchElementException。这和「先瞄一眼再动手」是一个道理。
for-each 其实就是迭代器语法糖
你写的增强 for,编译器会把它翻译成迭代器版本。所以下面两段完全等价:
// 写法一:增强 for(最常用)
for (String s : list) {
System.out.println(s);
}
// 写法二:等价于它底层的迭代器
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next();
System.out.println(s);
}
```bash
## 遍历中删元素:唯一的坑
这是初学者最高频的崩溃点。你想在遍历时删掉某些元素,于是这么写:
```java
// 错误写法!会抛 ConcurrentModificationException
for (String s : list) {
if (s.isEmpty()) {
list.remove(s); // 遍历中改了集合结构
}
}
集合在遍历时会盯着一个「修改计数」,你用集合自己的 remove 改了结构,计数对不上,它立刻抛 ConcurrentModificationException 来保护数据一致性。这叫 fail-fast(快速失败)。
正确做法是用迭代器自己的 remove:
// 正确写法:用迭代器删除
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().isEmpty()) {
it.remove(); // 安全,迭代器自己记录的删除
}
}
```bash
> [!WARNING]
> 遍历中除了用 `it.remove()`,不要用集合的 `add`/`remove`/`clear` 改结构。这条规则对 List、Set、Map(以及基于 entrySet 遍历时)全都适用,Map 遍历删键也要用 `iterator.remove()`。
## forEachRemaining 一次性消费
Java 8 起迭代器多了个 `forEachRemaining`,把剩下的元素一次性交给一个 lambda 处理:
```java
Iterator<String> it = list.iterator();
it.next(); // 先取走第一个
it.forEachRemaining(s -> System.out.println("剩下:" + s)); // 处理其余全部
ListIterator:能往前走的迭代器
普通 Iterator 只能往后走,而 List 提供的 ListIterator 还能往前、能改当前元素、能拿到下标:
ListIterator<String> lit = list.listIterator();
while (lit.hasNext()) {
int i = lit.nextIndex(); // 下一个的下标
String s = lit.next();
lit.set(s + "!"); // 就地修改
}
```bash
> [!TIP]
> 日常 99% 用普通 Iterator 足够。只有需要「往前遍历」或「边遍历边改当前元素」时才上 ListIterator,知道有这个能力即可。
## fail-fast 不是绝对保证
严格说,fail-fast 是「尽最大努力」检测,不保证每次冲突都立刻报错。千万别把「没抛异常」当成「这么写安全」的证据。遍历中改集合,永远走 `it.remove()` 这条路,最稳。
## Iterable 和 Iterator 的关系
一句话总结两者分工:`Iterable` 是「我能提供迭代器」,`Iterator` 是「具体的那把游标」。for-each 只认 `Iterable`;而 `Iterator` 负责实际走动。你让自己写的类实现 `Iterable` 时,只要在 `iterator()` 里 new 一个 `Iterator` 返回即可,剩下的遍历逻辑交给迭代器。
## 自己写一个可遍历的类
比如你想让「一串数字范围」能用 for-each,就让它实现 `Iterable`:
```java
class Range implements Iterable<Integer> {
private final int start, end;
Range(int s, int e) { start = s; end = e; }
public Iterator<Integer> iterator() {
return new Iterator<>() {
int cur = start;
public boolean hasNext() { return cur <= end; }
public Integer next() { return cur++; }
};
}
}
这样 for (int x : new Range(1, 5)) 就能直接用。理解这一步,你就懂了 for-each 不是集合专属,任何实现了 Iterable 的对象都行。
fail-fast 的底层原理(通俗版)
集合内部有个 modCount 计数器,每次结构修改(增、删、清)就加 1。迭代器创建时会记下当时的 modCount,每走一步都核对:发现对不上,说明遍历期间集合被从外部改了,立刻抛异常。它不是为了拦你,是怕你拿到错乱的数据还不知道。
Notefail-fast 是「尽力检测」,不保证每次冲突都报错。千万别把「没抛异常」当成「这么写安全」的证据,遍历中改结构永远走
it.remove()。
单线程也要小心
别以为 fail-fast 只在多线程才触发。单线程里 for (x : list) list.remove(x) 一样崩。规则很简单:遍历中改结构,唯一安全入口是 it.remove()。这条对 List、Set、Map 全都适用。
迭代器的 remove 只能调一次
注意一个细节:it.remove() 必须在 next() 之后调用,而且两次 next() 之间最多调一次 remove。连续调两次 remove 而不经过 next,会抛 IllegalStateException。它删的是「刚 next 出来的那个」,不是随便删,这个顺序约束要记牢。
迭代器不只在集合里
迭代器思想很通用。任何「一串可以按顺序取出的东西」都能用 Iterator 表达,比如你自定义的数据结构想支持 for-each,就让它实现 Iterable 并返回自己的 Iterator。所以 Iterator 是一种遍历协议,集合只是最常见的实现者。掌握它,你写的类也能拥有和集合一样顺滑的遍历体验,而不仅仅局限于 List、Set 这些现成容器。
小提醒:for-each 里不能改结构,但能改元素
fail-fast 限制的是「增删清这种结构变动」。如果你只是修改当前元素的某个字段(比如 it.next().setName("x")),那不属于结构变动,不会触发异常。所以「遍历中改属性」和「遍历中增删」是两码事,别一概而论,前者安全、后者才危险。
小结
Iterable 决定对象能不能被 for-each 遍历,Iterator 是遍历的游标(hasNext/next/remove)。增强 for 本质是迭代器语法糖。遍历中删元素必须用 it.remove(),否则触发 fail-fast 抛 ConcurrentModificationException。下一章看让元素能排序的 Comparable 与 Comparator。