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类型的迭代——list、set、queue等——都具有使用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);
最后,让我们看一个也是Collection的Queue:
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来迭代Map的EntrySet。
由于 Map的条目存储在名为EntrySet的Set中,我们可以使用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()*方法,但使迭代工作的底层代码使用这些方法。这意味着这些操作的复杂性对程序员来说是隐藏的,但它仍然存在。
与集合本身进行迭代的内部迭代器相反,这里我们需要将每个元素从集合中取出的外部代码。