Contents

forEach 循环简介

1. 概述

在 Java 8 中引入的forEach循环为程序员提供了一种新的、简洁且有趣的方式来迭代集合。

在本教程中,我们将了解如何将forEach与集合一起使用,它需要什么样的参数,以及此循环与增强的 for-loop有何不同。

2. forEach的基础知识

在 Java 中,Collection 接口以 Iterable作为其超接口。这个接口有一个从 Java 8 开始的新 API:

void forEach(Consumer<? super T> action)

简单地说,forEach 的Javadoc声明 “对Iterable的每个元素执行给定的操作,直到所有元素都被处理或该操作引发异常”。

因此,使用forEach,我们可以迭代集合并对每个元素执行给定的操作,就像任何其他Iterator一样。

例如,考虑迭代和打印字符串集合的for 循环版本:

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

我们可以使用forEach来编写:

names.forEach(name -> {
    System.out.println(name);
});

3. 使用forEach方法

我们使用 forEach遍历集合并对每个元素执行特定操作。要执行的操作包含在实现Consumer接口的类中,并作为参数传递给forEach

Consumer接口是一个功能接口 (具有单个抽象方法的接口)。它接受输入并且不返回结果。

这是定义:

@FunctionalInterface
public interface Consumer {
    void accept(T t);
}

因此,任何实现,例如,简单地打印String的消费者:

Consumer<String> printConsumer = new Consumer<String>() {
    public void accept(String name) {
        System.out.println(name);
    };
};

可以作为参数传递给forEach

names.forEach(printConsumer);

但这不是通过消费者创建操作并使用forEach API 的唯一方法。

让我们看看我们使用forEach方法的三种最流行的方式。

3.1. 匿名Consumer实现

我们可以使用匿名类实例化Consumer接口的实现,然后将其作为参数应用到forEach方法:

Consumer<String> printConsumer= new Consumer<String>() {
    public void accept(String name) {
        System.out.println(name);
    }
};
names.forEach(printConsumer);

这很好用。但是如果我们分析这个例子,我们会发现有用的部分实际上是*accept()*方法中的代码。

尽管 Lambda 表达式现在是规范和更简单的方法,但仍然值得了解如何实现Consumer接口。

3.2. Lambda 表达式

Java 8 函数式接口的主要好处是我们可以使用 Lambda 表达式来实例化它们并避免使用笨重的匿名类实现。

由于Consumer Interface 是一个函数式接口,我们可以用 Lambda 来表达:

(argument) -> { //body }

因此,我们的printConsumer被简化了:

name -> System.out.println(name)

我们可以将它传递给 forEach

names.forEach(name -> System.out.println(name));

自从在 Java 8 中引入 Lambda 表达式以来,这可能是使用forEach方法的最常见方式。

Lambda 确实有一个非常真实的学习曲线,所以如果你刚开始, 这篇 文章 会介绍一些使用新语言功能的良好实践。

3.3. 方法参考

我们可以使用方法引用语法而不是普通的 Lambda 语法,其中已经存在一个方法来对类执行操作:

names.forEach(System.out::println);

4. 使用forEach

4.1. 迭代集合

任何 Collection类型的迭代——listsetqueue等——都具有使用forEach 的相同语法。

因此,正如我们所见,我们可以这样迭代列表的元素:

List<String> names = Arrays.asList("Larry", "Steve", "James");
names.forEach(System.out::println);

和一组类似:

Set<String> uniqueNames = new HashSet<>(Arrays.asList("Larry", "Steve", "James"));
uniqueNames.forEach(System.out::println);

最后,让我们看一个也是CollectionQueue

Queue<String> namesQueue = new ArrayDeque<>(Arrays.asList("Larry", "Steve", "James"));
namesQueue.forEach(System.out::println);

4.2. 使用 Map 的forEach迭代

地图不是Iterable,但它们确实提供了自己的forEach变体,它接受BiConsumer

Java 8在 Iterable 的forEach中引入了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) -> System.out.println(key + " " + value));

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

(key, value) -> System.out.println(key + " " + value)

4.3. 通过entrySet来迭代

我们还可以使用 Iterable 的forEach来迭代MapEntrySet

由于 Map的条目存储在名为EntrySetSet中,我们可以使用forEach**对其进行迭代:

namesMap.entrySet().forEach(entry -> System.out.println(
  entry.getKey() + " " + entry.getValue()));

5. Foreach 与 For 循环

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

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

5.1. 内部迭代器——forEach

这种类型的迭代器在后台管理迭代,让程序员只需编写集合元素的代码即可。

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

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

names.forEach(name -> System.out.println(name));

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

5.2. 外部迭代器——for循环

外部迭代器混合了循环的完成内容方式

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

考虑这个熟悉的循环:

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

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

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