1. 概述

Java 8 中引入的 forEach() 方法为程序员提供了一种简洁的遍历集合的方式

在本教程中,我们将了解如何使用 forEach() 方法处理集合,它接受什么类型的参数,以及这个循环与增强的for 循环有何不同。

如果你需要复习一些 Java 8 的概念,可以查看我们的文章集合

2. forEach() 的基础

在 Java 中,Collection 接口继承自 Iterable 接口。从 Java 8 开始,这个接口有了一个新的 API:

void forEach(Consumer<? super T> action)

简单来说,forEachJavadoc 声明它"对 Iterable 的每个元素执行给定的操作,直到所有元素都被处理完毕或操作抛出异常为止"。

因此,使用 *forEach()*,我们可以遍历集合并对每个元素执行给定的操作。

例如,让我们考虑一个遍历并打印 String 集合的增强 for 循环版本:

List names = List.of("Larry", "Steve", "James", "Conan", "Ellen");

for (String name : names) {
    LOG.info(name);
}

我们可以使用 forEach() 来编写:

names.forEach(name -> {
    LOG.info(name);
});

在这里,我们在集合上调用 forEach() 并将名称记录到控制台。

3. 将 forEach() 方法与集合一起使用

forEach() 方法符合 Java 函数式编程范式,使代码更具声明性。

3.1. 遍历 List

forEach() 方法可以用于列表:

List names = List.of("Larry", "Steve", "James", "Conan", "Ellen");
names.forEach(name -> logger.info(name));

上面的代码将集合的所有元素记录到控制台。

3.2. 使用 forEach() 遍历 Map

Map 非 Iterable其提供了自己的 forEach() 变体,它接受一个 BiConsumer 类型的参数。

Java 8 在 MapforEach() 中引入了 BiConsumer 而不是 Consumer,这样就可以同时对 Map 的键和值执行操作。

下面我们创建一个 Map 对象:

Map<Integer, String> namesMap = new HashMap<>();
namesMap.put(1, "Larry");
namesMap.put(2, "Steve");
namesMap.put(3, "James");

接下来,让我们使用 Map 的 forEach() 遍历 namesMap

namesMap.forEach((key, value) -> LOG.info(key + " " + value));

正如我们在这里看到的,我们使用了 BiConsumer 来遍历 Map 的条目。

3.3. 通过遍历 entrySet() 来遍历 Map

我们还可以使用 Iterable 的 forEach() 来遍历 MapEntrySet

由于 Map 的元素存储在一个名为 EntrySetSet 中,我们可以使用 forEach() 来遍历它

namesMap.entrySet().forEach(entry -> LOG.info(entry.getKey() + " " + entry.getValue()));

3.4. 使用 forEach() 方法进行并行操作

对于大型集合,将 forEach() 与并行流一起使用可以通过利用多个 CPU 内核来提高性能:

List names = List.of("Larry", "Steve", "James", "Conan", "Ellen");
names.parallelStream().forEach(LOG::info);

上面的代码并行运行。但是,并行执行可能会增加资源消耗。

4. forEach() 的限制

尽管 forEach() 方法很方便,但它也有局限性。

4.1. 不能直接在数组上调用

我们不能直接在数组上调用该方法:

String[] foodItems = {"rice", "beans", "egg"};
foodItems.forEach(food -> logger.info(food));

上面的代码无法编译,因为数组没有 forEach() 方法。但是,我们可以通过将数组转换为流来使其编译:

Arrays.stream(foodItems).forEach(food -> logger.info(food));

由于流具有 forEach() 方法,我们可以将数组转换为流并遍历其元素。

4.2. 不能修改集合本身

此外,我们不能使用该方法修改集合本身:

List<String> names = List.of("Larry", "Steve", "James", "Conan", "Ellen");
names.forEach(name -> {
    if (name.equals("Larry")) {
        names.remove(name);
    }
});

上面的代码会抛出 ConcurrentModificationException 错误,因为在用 forEach() 遍历集合时不允许修改集合。与传统的 for 循环不同,它允许通过谨慎的索引进行修改。

4.3. 不能中断或继续循环

与传统的 for 循环不同,forEach() 不支持 breakcontinue

List<String> names = List.of("Larry", "Steve", "James", "Conan", "Ellen");
names.forEach(name -> {
    if (name.equals("Steve")) {
        break;
    }
    logger.info(name);
});

上面的代码会抛出异常。

4.4. 不允许计数器

forEach() 方法不支持在迭代期间修改计数器变量:

List<String> names = List.of("Larry", "Steve", "James", "Conan", "Ellen");
int count = 0;
names.forEach(name -> {
    count++;
});

上面的代码会导致编译错误,因为 lambda 表达式要求在其中使用的变量是 final 的,这意味着它们的值在初始化后不能被修改。

但是,我们可以使用原子变量代替,它允许在 lambda 表达式内部进行修改。

4.5. 不能访问下一个或上一个元素

此外,我们可以使用传统的 for 循环来引用集合的前一个或下一个元素:

List<String> names = List.of("Larry", "Steve", "James", "Conan", "Ellen");
for (int i = 0; i < names.size(); i++) {
    String current = names.get(i);
    String previous = (i > 0) ? names.get(i - 1) : "None";
    String next = (i < names.size() - 1) ? names.get(i + 1) : "None";

    LOG.info("Current: {}, Previous: {}, Next: {}", current, previous, next);
}

在上面的代码中,我们使用索引 i 来确定前一个 (i – 1) 和下一个 (i + 1) 元素。

但是,这在 forEach() 方法中是不可能的,因为它单独处理元素而不暴露它们的索引。

5. forEach() 与传统 for 循环的比较

两者都可以遍历集合和数组。然而,forEach() 方法不如传统的 for 循环灵活。

for 循环允许我们明确定义循环控制变量、条件和增量,而 forEach() 方法则抽象了这些细节:

List names = List.of("Larry", "Steve", "James", "Conan", "Ellen");
for (int i = 0; i < names.size(); i++) {
    LOG.info(names.get(i));
}

我们还可以修改循环条件:

for (int i = 0; i < names.size() - 1; i++) {
    LOG.info(names.get(i));
}

在上面的代码中,我们通过修改循环条件 - names.size() – 1 来跳过集合中的最后一个元素。这种灵活性级别在 forEach() 方法中是不可能的。

forEach() 方法允许我们对集合元素执行操作,但不允许修改集合本身。

for 循环允许我们对集合的单个元素执行操作,并允许我们修改集合本身。

6. forEach() 与增强 for 循环的比较

从简单的角度来看,两种循环提供相同的功能:遍历集合中的元素。

*它们之间的主要区别在于它们是不同的迭代器。增强的 for 循环是外部迭代器,而新的 forEach 方法是内部迭代器。*

6.1. 内部迭代器 – forEach()

这种类型的迭代器在后台管理迭代,让程序员只需编写要对集合元素执行的操作。

迭代器管理迭代并确保逐个处理元素。

让我们看一个内部迭代器的例子:

List<String> names = List.of("Larry", "Steve", "James", "Conan", "Ellen");
names.forEach(name -> LOG.info(name));

在上面的 forEach 方法中,我们可以看到提供的参数是一个 lambda 表达式。这意味着该方法只需要知道要做什么,而所有迭代工作将在内部处理。

6.2. 外部迭代器 – for 循环

外部迭代器混合了循环的目的和方式。

EnumerationsIterators 和增强的 for 循环 都是外部迭代器(还记得 iterator()next()hasNext() 方法吗?)。在所有这些迭代器中,我们的工作是指定如何执行迭代。

考虑这个熟悉的循环:

List<String> names = List.of("Larry", "Steve", "James", "Conan", "Ellen");
for (String name : names) {
    LOG.info(name);
}

虽然我们在遍历列表时没有显式调用 hasNext()next() 方法,但使这个迭代工作的底层代码使用了这些方法。这意味着这些操作的复杂性对程序员来说是隐藏的,但它仍然存在。

与集合本身执行迭代的内部迭代器相反,这里我们需要外部代码来取出集合中的每个元素。

7. 总结

在本文中,我们展示了 forEach 循环比普通的 for 循环 更方便。

我们还了解了 forEach 方法的工作原理,以及可以接受什么样的实现作为参数来对集合中的每个元素执行操作。


原始标题:Guide to the Java forEach Loop | Baeldung