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 知识问答

  1. 什么叫遍历集合?
  2. 为什么 Collection 不能统一使用普通索引 for?
  3. Iterator 是什么?
  4. Collection 为什么可以调用 iterator()
  5. IterableIterator 有什么区别?
  6. hasNext() 的作用是什么?
  7. next() 的作用是什么?
  8. 为什么 next() 通常要配合 hasNext()
  9. 如果没有下一个元素仍然调用 next(),可能出现什么异常?
  10. 为什么一次循环中不应该随意调用多次 next()
  11. 增强 for 的完整语法是什么?
  12. 增强 for 可以遍历哪两类典型结构?
  13. 增强 for 遍历 Collection 与 Iterator 有什么关系?
  14. forEach 方法实际来自哪个接口?
  15. Lambda 在 forEach 中承担什么角色?
  16. System.out::println 是什么语法?
  17. Collection 的 forEach 与 Stream 是不是同一个东西?
  18. 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);
}

要求:

  1. 第一次调用 hasNext() 时会发生什么?
  2. 第一次 next() 返回什么?
  3. 第二次 next() 返回什么?
  4. 当三个元素都处理完以后,hasNext() 返回什么?
  5. 如果此时继续调用 next(),可能发生什么?

阅读:

Iterator<String> iterator =
        names.iterator();

while (iterator.hasNext()) {

    System.out.println(
            iterator.next()
    );

    System.out.println(
            iterator.next()
    );
}

回答:

  1. 一轮循环为什么消费了两个元素?
  2. 如果集合有偶数个元素,会产生什么遍历效果?
  3. 如果集合有奇数个元素,最后可能出现什么问题?
  4. 如何修改成更加规范的写法?

阅读:

for (String name : names) {
    System.out.println(name);
}

回答:

  1. name 表示什么?
  2. 每一轮循环中的 name 是否表示同一个集合位置?
  3. 对 Collection 而言,这种遍历与哪个接口机制有关?

7.3 手写代码

  1. 创建 Collection<String>,保存 5 个技术名称。
  2. 使用 Iterator 遍历并打印。
  3. 使用增强 for 再遍历一次。
  4. 使用 Lambda + forEach 再遍历一次。
  5. 使用 System.out::println 再遍历一次。
  6. 不使用 IDE 自动生成代码,完整手写。

创建:

Collection<Student>

要求:

  1. 保存至少 3 个 Student 对象。
  2. 使用 Iterator 输出所有对象。
  3. 使用增强 for 输出所有对象。
  4. 使用 forEach 输出所有对象。

7.4 Debug

下面代码:

Iterator<String> iterator =
        names.iterator();

while (true) {
    System.out.println(
            iterator.next()
    );
}

要求:

  1. 说明错误思想。
  2. 预测最终可能出现什么异常。
  3. 修改为标准 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);
}

要求:

  1. 找出潜在逻辑问题。
  2. 说明 next() 的调用次数与元素消费数量之间的关系。
  3. 修改代码,使每轮循环只处理一个元素。

7.5 综合训练

设计一个“课程列表展示程序”。

数据:

JavaSE
MySQL
JavaWeb
Spring
Redis

要求分别使用:

Iterator

增强 for

forEach + Lambda

forEach + 方法引用

完成遍历。

然后回答:

  1. 哪一种写法最显式地控制迭代过程?
  2. 哪一种最适合普通只读遍历?
  3. 哪一种写法最具有函数式风格?
  4. 为什么不同遍历方式可以共同作用于同一个 Collection?

7.6 本章验收

闭卷完成以下要求:

  • 能够写出 Iterator<E> 的基本使用模板。
  • 能够准确说明 hasNext()next() 的区别。
  • 能够解释 NoSuchElementException 的典型出现原因。
  • 能够手写增强 for。
  • 能够解释增强 for 遍历 Collection 与 Iterator 的关系。
  • 能够手写 collection.forEach(...)
  • 能够看懂 System.out::println
  • 能够解释 IterableIterator 的区别。
  • 能够根据需求选择 Iterator、增强 for 或 forEach。

如果仍然需要依赖 IDE 自动补全才能写出 Iterator 标准遍历,本章还没有达到真正掌握的标准。