Iterator、增强 for 与 forEach 遍历 | JavaSE
Iterator、增强 for 与 forEach 遍历
一、学习目标
完成本章后,你应该能够:
- 能够解释什么是迭代器(Iterator),以及它为什么可以统一遍历不同类型的集合。
- 能够使用
iterator()、hasNext()、next()完成 Collection 集合遍历。 - 能够理解迭代器遍历过程中“元素”和“迭代位置”的变化。
- 能够使用增强 for 循环遍历数组和集合。
- 能够说明增强 for 遍历 Collection 与 Iterator 之间的关系。
- 能够使用
forEach、Lambda 表达式以及方法引用完成集合遍历。 - 能够根据场景选择 Iterator、增强 for、forEach,而不是机械地只使用其中一种方式。
二、核心知识
2.1 什么叫遍历集合
所谓遍历(Traversal),就是:
按照集合提供的访问规则,把集合中的元素一个一个取出来进行处理。
例如集合:
Collection<String> names = new ArrayList<>();
names.add("张无忌");
names.add("玄冥二老");
names.add("宋青书");
如果我们希望:
张无忌
玄冥二老
宋青书
依次处理每个元素,就需要进行遍历。
遍历时可以:
打印元素
统计数量
查找数据
判断条件
转换数据
执行某种业务逻辑
例如:
遍历所有学生
→ 输出学生信息
遍历所有商品
→ 判断是否库存不足
遍历所有订单
→ 统计总金额
因此:
遍历是集合最基础、最重要的操作之一。
2.2 为什么 Collection 不能统一使用普通索引 for
数组可以这样遍历:
String[] names = {
"张三",
"李四",
"王五"
};
for (int i = 0; i < names.length; i++) {
System.out.println(names[i]);
}
因为数组天然存在:
0
1
2
3
...
这样的索引。
但是 Collection 是一个更高层次的接口。
它下面可能存在:
ArrayList
LinkedList
HashSet
TreeSet
...
并不是所有集合都应该暴露:
get(index)
这种基于索引的访问方式。
例如:
List
→ 有位置概念
Set
→ 不提供普通 List 式索引访问
因此 Collection 需要一种:
不依赖具体底层数据结构的统一遍历方案。
这就是:
Iterator
2.3 Iterator 是什么
Iterator<E>:
迭代器。
可以把它理解成一个专门负责:
“沿着集合提供的遍历顺序,一个一个访问元素”
的对象。
例如:
Iterator<String> iterator = names.iterator();
这里:
names.iterator()
表示:
从
names集合中获取一个用于遍历该集合的迭代器。
因此基本关系可以理解为:
Collection
│
│ iterator()
↓
Iterator
│
├── hasNext()
└── next()
2.4 Collection 为什么能获得 Iterator
前面学习集合体系时已经知道:
Iterable
↓
Collection
也就是说:
Collection
继承了:
Iterable
而 Iterable<T> 的核心能力之一就是:
Iterator<T> iterator();
因此:
Collection<String> names = new ArrayList<>();
可以调用:
names.iterator();
得到一个:
Iterator<String>
对象。
这也是 Java 集合统一遍历机制的重要基础。
2.5 Iterator 最核心的三个方法
初学 Iterator 时,重点掌握三个方法。
| 方法 | 作用 |
| ------------ | ------------------------------ |
| iterator() | 获取集合对应的迭代器 |
| hasNext() | 判断是否还有下一个元素 |
| next() | 返回下一个元素,并推进迭代状态 |
典型代码:
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
String name = iterator.next();
System.out.println(name);
}
这是 Iterator 最标准的遍历模板。
2.6 hasNext() 的作用
方法:
boolean hasNext()
含义:
如果继续调用
next()还能得到一个元素,则返回true。
例如:
while (iterator.hasNext()) {
...
}
可以理解为:
还有元素吗?
│
├── 有 → true → 继续读取
│
└── 没有 → false → 结束循环
2.7 next() 的作用
方法:
E next()
负责:
返回迭代过程中的下一个元素,并推进迭代器状态。
例如:
String name = iterator.next();
第一次调用可能得到:
张无忌
第二次:
玄冥二老
第三次:
宋青书
最终:
iterator.hasNext()
返回:
false
2.8 用“游标”理解 Iterator
可以把 Iterator 想象成一个不断向前移动的游标。
集合:
张无忌 玄冥二老 宋青书
最开始:
^
张无忌 玄冥二老 宋青书
执行:
iterator.next();
得到:
张无忌
然后迭代状态继续向后推进:
^
张无忌 玄冥二老 宋青书
再次:
iterator.next();
得到:
玄冥二老
继续:
^
张无忌 玄冥二老 宋青书
直到所有元素都被访问。
这只是一个帮助理解的模型。
真正使用时只需要记住:
hasNext()
→ 问还有没有
next()
→ 取下一个
三、使用方法
3.1 Iterator 标准遍历写法
完整示例:
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class IteratorDemo {
public static void main(String[] args) {
Collection<String> names =
new ArrayList<>();
names.add("张无忌");
names.add("玄冥二老");
names.add("宋青书");
Iterator<String> iterator =
names.iterator();
while (iterator.hasNext()) {
String name =
iterator.next();
System.out.println(name);
}
}
}
核心模板:
Iterator<元素类型> iterator =
集合.iterator();
while (iterator.hasNext()) {
元素类型 element =
iterator.next();
// 处理 element
}
建议达到:
不需要 IDE 提示,也能闭卷手写。
3.2 为什么必须先 hasNext 再 next
错误写法:
Iterator<String> iterator =
names.iterator();
System.out.println(iterator.next());
System.out.println(iterator.next());
System.out.println(iterator.next());
System.out.println(iterator.next());
如果集合只有三个元素,第四次:
next()
已经没有元素可以返回。
此时可能抛出:
NoSuchElementException
因此标准写法是:
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
逻辑就是:
先确认还有元素
↓
再读取元素
3.3 不要在一次循环中随意调用多次 next()
例如:
Iterator<String> iterator =
names.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
System.out.println(iterator.next());
}
很多初学者会误以为:
第二次 next()
只是再次获取刚才那个元素
不是。
每调用一次:
next()
都会继续消费一个元素。
假设集合:
A B C D
第一次循环:
next()
→ A
next()
→ B
第二次循环:
next()
→ C
next()
→ D
如果元素数量是奇数,例如:
A B C
就可能在最后一次循环中产生:
NoSuchElementException
因此更清晰的写法是:
while (iterator.hasNext()) {
String value =
iterator.next();
System.out.println(value);
}
一次循环通常只取得一次当前元素。
3.4 Iterator 能遍历哪些 Collection
例如:
List<String>
Set<String>
Queue<String>
只要属于可迭代体系,就可以通过:
iterator()
获得迭代器。
因此同一种代码思想可以应用于:
ArrayList
LinkedList
HashSet
LinkedHashSet
TreeSet
...
这就是 Iterator 的价值:
统一不同集合实现的遍历方式。
3.5 增强 for 循环
Java 提供了一种非常简洁的遍历语法:
增强 for(Enhanced for Statement)。
格式:
for (元素类型 变量 : 数组或者可迭代对象) {
}
例如:
Collection<String> names =
new ArrayList<>();
names.add("张无忌");
names.add("玄冥二老");
names.add("宋青书");
for (String name : names) {
System.out.println(name);
}
可以直接理解为:
对于 names 中的每一个 String
都把当前元素临时交给 name
然后执行循环体
3.6 增强 for 可以遍历数组
例如:
String[] names = {
"张三",
"李四",
"王五"
};
for (String name : names) {
System.out.println(name);
}
因此增强 for 常见两类使用对象:
数组
实现 Iterable 的对象
Collection 就属于后者。
3.7 增强 for 遍历 Collection 与 Iterator 的关系
对于 Collection 等实现 Iterable 的对象:
for (String name : names) {
System.out.println(name);
}
其遍历机制建立在:
Iterator
之上。
可以从学习模型上理解为类似:
Iterator<String> iterator =
names.iterator();
while (iterator.hasNext()) {
String name =
iterator.next();
System.out.println(name);
}
因此:
增强 for 并没有创造一个完全不同的 Collection 遍历体系,而是让迭代式遍历写得更加简洁。
3.8 增强 for 的优势
Iterator:
Iterator<String> iterator =
names.iterator();
while (iterator.hasNext()) {
String name =
iterator.next();
System.out.println(name);
}
增强 for:
for (String name : names) {
System.out.println(name);
}
后者明显更加:
简洁
直观
适合只读取元素
因此:
如果只是遍历并读取数据,增强 for 是非常常见的选择。
3.9 Collection.forEach
Java 还支持:
names.forEach(...);
例如:
Collection<String> names =
new ArrayList<>();
names.add("张无忌");
names.add("玄冥二老");
names.add("宋青书");
names.forEach(
name -> System.out.println(name)
);
这里:
name -> System.out.println(name)
是 Lambda 表达式。
含义是:
对集合中的每个元素执行一次打印操作。
3.10 forEach 的底层接口来源
严格来说:
forEach
定义在:
Iterable
接口中。
由于:
Iterable
↓
Collection
所以所有 Collection 都可以获得这一能力。
方法签名:
default void forEach(
Consumer<? super T> action
)
当前只需要理解:
forEach
→ 遍历每个元素
Consumer
→ 接收一个元素并执行某个操作
关于:
Consumer
Lambda
函数式接口
会在后面的 Lambda 专章系统学习。
3.11 使用 Lambda 遍历
例如:
names.forEach(
name -> {
System.out.println(
"当前姓名:" + name
);
}
);
如果 Lambda 只有一句:
System.out.println(name);
可以简化:
names.forEach(
name -> System.out.println(name)
);
3.12 使用方法引用
下面代码:
names.forEach(
name -> System.out.println(name)
);
还可以进一步写成:
names.forEach(
System.out::println
);
这里:
System.out::println
是:
方法引用(Method Reference)。
当前先认识。
后续:
06-04 方法引用
会系统解释。
3.13 三种遍历方式完整对比
准备集合:
Collection<String> names =
new ArrayList<>();
names.add("张无忌");
names.add("玄冥二老");
names.add("宋青书");
Iterator
Iterator<String> iterator =
names.iterator();
while (iterator.hasNext()) {
String name = iterator.next();
System.out.println(name);
}
增强 for
for (String name : names) {
System.out.println(name);
}
forEach + Lambda
names.forEach(
name -> System.out.println(name)
);
forEach + 方法引用
names.forEach(
System.out::println
);
四、原理与进阶
4.1 Iterable 与 Iterator 是两个不同概念
很多初学者容易混淆:
Iterable
与:
Iterator
Iterable
表示:
一个对象具备“可以被迭代”的能力。
核心方法:
iterator()
Iterator
表示:
真正负责一次遍历过程的迭代器对象。
核心方法:
hasNext()
next()
因此关系是:
Iterable
│
│ iterator()
↓
Iterator
可以类比:
图书馆
→ Iterable
从图书馆领取的一张取书路线
→ Iterator
同一个 Collection:
Collection<String> names
甚至可以多次调用:
names.iterator();
获得不同的 Iterator 对象。
4.2 Iterator 的状态属于一次遍历过程
例如:
Iterator<String> it1 =
names.iterator();
Iterator<String> it2 =
names.iterator();
此时:
it1
和:
it2
是两个独立的迭代器引用。
it1.next() 并不会直接意味着:
it2 也自动推进
它们代表不同的迭代过程。
4.3 Iterator 还有 forEachRemaining
Iterator 在现代 Java 中还提供:
forEachRemaining(...)
例如:
Iterator<String> iterator =
names.iterator();
if (iterator.hasNext()) {
System.out.println(
"第一个:" + iterator.next()
);
}
iterator.forEachRemaining(
System.out::println
);
含义:
对迭代器中尚未访问的剩余元素逐个执行操作。
它不是本章必须重点背诵的 API,但可以帮助理解:
Iterator 自身也可以表达“继续处理剩余元素”
4.4 forEach 并不是 Stream
下面:
names.forEach(...)
不是:
Stream API
它来自:
Iterable
而后面会学习:
names.stream()
.filter(...)
.map(...)
.forEach(...);
那才是:
Stream 数据处理流水线
因此:
Collection.forEach
≠ Stream 本身
不要因为都出现 forEach 就混为一谈。
五、实践应用
5.1 输出文章标题
例如:
Collection<String> articleTitles =
new ArrayList<>();
articleTitles.add("Java 集合框架");
articleTitles.add("Java 泛型");
articleTitles.add("Java IO");
简单展示:
for (String title : articleTitles) {
System.out.println(title);
}
5.2 检查学生成绩
Collection<Integer> scores =
new ArrayList<>();
scores.add(95);
scores.add(78);
scores.add(61);
for (Integer score : scores) {
if (score >= 60) {
System.out.println(
score + ":及格"
);
}
}
5.3 使用 forEach 输出对象
Collection<String> technologies =
new ArrayList<>();
technologies.add("Java");
technologies.add("MySQL");
technologies.add("Redis");
technologies.forEach(
technology ->
System.out.println(
"正在学习:" + technology
)
);
六、常见问题
6.1 Iterator 是一个类吗?
不是简单意义上的普通实现类。
Iterator<E>
是:
接口
具体集合会返回相应的迭代器实现对象。
6.2 为什么 Iterator 不需要索引?
因为 Iterator 的目标就是:
屏蔽不同集合底层结构差异。
它只关注:
还有没有下一个元素?
下一个元素是什么?
而不要求:
“第几个索引是什么?”
6.3 hasNext 会取出元素吗?
不会。
hasNext()
只是判断:
还有没有下一个元素
真正获得元素的是:
next()
6.4 next 可以一直调用吗?
不能。
如果已经没有元素继续调用:
next()
可能抛出:
NoSuchElementException
因此一般配合:
hasNext()
使用。
6.5 增强 for 能获得索引吗?
增强 for 的设计重点是:
直接得到元素
例如:
for (String name : names) {
}
这里直接获得:
name
并没有直接提供:
index
如果后续使用 List,并且业务确实需要索引,则可以考虑普通索引 for。
这个内容放在:
05-05 List 接口与有序集合
继续学习。
6.6 增强 for 可以修改集合结构吗?
如果你在增强 for 遍历 Collection 的过程中:
names.remove(...)
或者:
names.add(...)
就可能遇到并发修改问题。
因此本章先记住:
增强 for 更适合“遍历并读取”。
遍历过程中修改集合的规则会在下一章专门学习。
6.7 forEach 中可以直接 remove 集合元素吗?
不要把:
collection.forEach(...)
理解成:
“可以在 Lambda 中随便修改这个集合。”
遍历过程中修改底层集合可能产生未定义的遍历行为,常见集合实现还可能检测到并发修改问题。
这个问题同样留到下一章系统解决。
6.8 三种遍历方式到底怎么选?
当前阶段可以使用下面的判断:
| 需求 | 推荐 |
| ------------------------------ | -------------------- |
| 普通读取所有元素 | 增强 for |
| 希望使用函数式写法 | forEach |
| 需要显式控制迭代过程 | Iterator |
| 遍历过程中需要安全删除当前元素 | Iterator,下一章学习 |
不要形成:
“forEach 最新,所以永远最好”
这样的思维。
不同写法解决的问题不同。
七、练习与验收
7.1 知识问答
- 什么叫遍历集合?
- 为什么 Collection 不能统一使用普通索引 for?
- Iterator 是什么?
- Collection 为什么可以调用
iterator()? Iterable与Iterator有什么区别?hasNext()的作用是什么?next()的作用是什么?- 为什么
next()通常要配合hasNext()? - 如果没有下一个元素仍然调用
next(),可能出现什么异常? - 为什么一次循环中不应该随意调用多次
next()? - 增强 for 的完整语法是什么?
- 增强 for 可以遍历哪两类典型结构?
- 增强 for 遍历 Collection 与 Iterator 有什么关系?
forEach方法实际来自哪个接口?- Lambda 在
forEach中承担什么角色? System.out::println是什么语法?- Collection 的
forEach与 Stream 是不是同一个东西? - Iterator、增强 for、forEach 各适合什么场景?
7.2 代码阅读
阅读:
List<String> names =
new ArrayList<>();
names.add("A");
names.add("B");
names.add("C");
Iterator<String> it =
names.iterator();
while (it.hasNext()) {
String name =
it.next();
System.out.println(name);
}
要求:
- 第一次调用
hasNext()时会发生什么? - 第一次
next()返回什么? - 第二次
next()返回什么? - 当三个元素都处理完以后,
hasNext()返回什么? - 如果此时继续调用
next(),可能发生什么?
阅读:
Iterator<String> iterator =
names.iterator();
while (iterator.hasNext()) {
System.out.println(
iterator.next()
);
System.out.println(
iterator.next()
);
}
回答:
- 一轮循环为什么消费了两个元素?
- 如果集合有偶数个元素,会产生什么遍历效果?
- 如果集合有奇数个元素,最后可能出现什么问题?
- 如何修改成更加规范的写法?
阅读:
for (String name : names) {
System.out.println(name);
}
回答:
name表示什么?- 每一轮循环中的
name是否表示同一个集合位置? - 对 Collection 而言,这种遍历与哪个接口机制有关?
7.3 手写代码
- 创建
Collection<String>,保存 5 个技术名称。 - 使用 Iterator 遍历并打印。
- 使用增强 for 再遍历一次。
- 使用 Lambda +
forEach再遍历一次。 - 使用
System.out::println再遍历一次。 - 不使用 IDE 自动生成代码,完整手写。
创建:
Collection<Student>
要求:
- 保存至少 3 个 Student 对象。
- 使用 Iterator 输出所有对象。
- 使用增强 for 输出所有对象。
- 使用
forEach输出所有对象。
7.4 Debug
下面代码:
Iterator<String> iterator =
names.iterator();
while (true) {
System.out.println(
iterator.next()
);
}
要求:
- 说明错误思想。
- 预测最终可能出现什么异常。
- 修改为标准 Iterator 遍历。
下面代码:
Iterator<String> iterator =
names.iterator();
while (iterator.hasNext()) {
String first =
iterator.next();
String second =
iterator.next();
System.out.println(first);
System.out.println(second);
}
要求:
- 找出潜在逻辑问题。
- 说明
next()的调用次数与元素消费数量之间的关系。 - 修改代码,使每轮循环只处理一个元素。
7.5 综合训练
设计一个“课程列表展示程序”。
数据:
JavaSE
MySQL
JavaWeb
Spring
Redis
要求分别使用:
Iterator
增强 for
forEach + Lambda
forEach + 方法引用
完成遍历。
然后回答:
- 哪一种写法最显式地控制迭代过程?
- 哪一种最适合普通只读遍历?
- 哪一种写法最具有函数式风格?
- 为什么不同遍历方式可以共同作用于同一个 Collection?
7.6 本章验收
闭卷完成以下要求:
- 能够写出
Iterator<E>的基本使用模板。 - 能够准确说明
hasNext()与next()的区别。 - 能够解释
NoSuchElementException的典型出现原因。 - 能够手写增强 for。
- 能够解释增强 for 遍历 Collection 与 Iterator 的关系。
- 能够手写
collection.forEach(...)。 - 能够看懂
System.out::println。 - 能够解释
Iterable与Iterator的区别。 - 能够根据需求选择 Iterator、增强 for 或 forEach。
如果仍然需要依赖 IDE 自动补全才能写出 Iterator 标准遍历,本章还没有达到真正掌握的标准。