1. 概述
在Java 8中引入的forEach循环为程序员提供了一种新的、简洁的方式来迭代集合。
在本教程中,我们将了解如何将forEach与集合一起使用,它需要什么样的参数,以及此循环与增强for循环有何不同。
2. forEach的基础知识
在Java中,Collection接口以Iterable作为其父接口。这个接口在Java 8中添加了一个新的API:
void forEach(Consumer<? super T> action)
简单地说,forEach的Java文档声明它”对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);
}
因此,任何Consumer接口的实现,例如简单地打印String的Consumer:
Consumer<String> printConsumer = new Consumer<String>() {
public void accept(String name) {
System.out.println(name);
};
};
都可以作为参数传递给forEach:
names.forEach(printConsumer);
但这并不是通过Consumer创建操作并使用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是一个函数式接口,我们可以用Lambda来表达它:
(argument) -> { //body }
因此,我们的printConsumer可以简化成以下方式:
name -> System.out.println(name)
我们可以将其传递给forEach:
names.forEach(name -> System.out.println(name));
自从在Java 8中引入Lambda表达式以来,这可能是使用forEach方法的最常见方式。
3.3 方法引用
我们也可以使用方法引用语法而不是普通的Lambda语法,其中我们引用的是一个已经存在的方法来对类执行操作:
names.forEach(System.out::println);
4. 使用forEach
4.1 迭代集合
任何实现Collection类型的集合(list、set、queue等)都具有使用forEach的相同语法。
因此,正如我们所看到的,我们可以这样迭代List集合的元素:
List<String> names = Arrays.asList("Larry", "Steve", "James");
names.forEach(System.out::println);
Set集合类似:
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 使用forEach迭代Map
Map并没有实现Iterable,但它确实提供了自己的forEach变体,Map中的forEach方法采用BiConsumer作为参数。
Java 8在Iterable的forEach中引入了BiConsumer而不是Consumer,以便可以同时对Map的key和value执行操作。
让我们创建一个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来迭代Map
我们还可以使用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表达式。这意味着该方法只需要知道要做什么,所有的迭代工作都将在提供的lambda内部处理。
5.2 外部迭代器-for循环
外部迭代器混合了循环应该做什么,并且应该怎么做。
Enumerations、Iterators和增强for循环都是外部迭代器(参考iterator()、next()或hasNext()方法)。在所有这些迭代器中,我们的任务是指定如何执行迭代。
考虑这个熟悉的循环:
for (String name : names) {
System.out.println(name);
}
虽然我们在迭代集合时没有显式调用hasNext()或next()方法,但使迭代工作的底层代码使用这些方法。这意味着这些操作的复杂性对程序员来说是隐藏的,但它仍然存在。
与内部迭代器相反,在该迭代器中,集合本身进行迭代,这里我们需要外部代码来从集合中取出每个元素。
6. 总结
在本文中,我们演示了forEach方法是如何工作的,以及可以接收什么样的实现作为参数,以便对集合中的每个元素执行操作。
与往常一样,本教程的完整源代码可在GitHub上获得。