概述
Java中对集合遍历存在多种方式,下面我们来看看Stream.forEach和Collection.forEach两种方式。
Collection.stream().forEach()和Collection.forEach()
通常情况下,两者都会产生相同的结果,但是,也有一下微妙的差异。
示例分析
首先,创建一个迭代列表:
List<String> list = Arrays.asList("A","B","C","D");
最直接的方法是使用增强的for循环:
for(String s: list) {
//do something with s
}
函数式编程
- 如果我们想使用函数式编程,可以使用forEach(),直接在集合上使用函数。
Consumer<String> consumer = s->{System.out::println};
list.forEach(consumer);
- 也可以在集合上调用forEach()
list.stream().forEach(consumer);
两个版本都迭代列表并打印所有元素:
ABCD
ABCD
在这个简单的例子中,我们使用的 forEach()没有区别。
执行顺序
Collection.forEach()使用集合的迭代器,集合元素的处理顺序是明确的。
但是Collection.stream.forEach()的处理顺序是不明确的。
在大多数情况下,我们选择上述两种方式是没有区别的,但是有时候还是有的。
Parallel Stream(并发流)
并发流允许我们在多个线程中执行stream,在这种情况下,执行顺序也不明确。Java只需要在调用任何最终操作(例如Collectin.toList())之前完成所有线程。
看一个例子,首先直接在集合上调用forEach(),然后再并发流上调用:
list.forEach(System.out::print);
list.parallalStream().forEach(System.out::print);
我们会看到 list.forEach()以插入顺序处理元素,而 list.parallelStream().forEach()在每次运行会产生不同的结果。一个可能的输出是:
ABCD CDBA
ABCD DBCA
自定义迭代器
让我们使用自定义迭代器定义一个列表,以反向顺序迭代集合:
class ReverseList extends ArrayList<String> {
@Override
public Iterator<String> iterator() {
int startIndex = this.size() -1;
List<String> list = this;
Iterator<String> it = new Iterator<String>() {
private int currentIndex = startIndex;
@Override
public String next() {
String next = list.get(currentIndex);
currentIndex--;
return next;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
retun it;
}
}
当我们遍历列表时,再次使用forEach()直接在集合上,然后再流上:
List<String> myList = new ReverseList();
myList.addAll(list);
myList.forEach(System.out::print);
myList.stream().forEach(System.out::print);
我们得到不同的结果:
DBCA ABCD
结果不同的原因是在列表中使用的forEach()会使用自定义迭代器,而stream().forEach()只是从列表中逐个取元素,会忽略迭代器。
修改集合
很多集合在遍历的时候,不应该在结构上被修改(比如 ArrayList 或 HashSet)。如果在迭代期间删除或添加元素,会抛出 ConcurrentModification 异常。
此外,集合设计为快速失败(fail-fast),这意味着一旦修改就抛出异常。
类似地,当我们在 stream 的执行期间添加或删除元素时,我们将得到 ConcurrentModification 异常。但是,异常将在稍后抛出。
两个 forEach()方法之间的另一个细微差别是 Java 明确允许使用迭代器修改元素。相反,stream 不能。来看一下更详细的例子。
删除元素
定义一个列表,删除最后一个元素(“D”):
遍历列表时,在打印第一个元素(“A”)后删除最后一个元素:
list.forEach(removeElement);
因为 forEach()是快速失败的,所以我们停止迭代并在处理下一个元素之前看到异常:
让我们看看如果我们使用 stream().forEach()会发生什么:
list.stream().forEach(removeElement);
在这里,我们继续迭代整个列表,然后才看到异常:
但是,Java 并不保证会抛出 ConcurrentModificationException。这意味着我们永远不应该编写依赖于此异常的程序。
改变元素
我们可以在迭代列表时更改元素:
list.forEach(e-> {
list.set(3,"E");
})
但是,虽然使用 Collection.forEach()或 stream().forEach()执行此操作没有问题,
但 Java 要求对流的操作是无干扰的。
这意味着在执行流管道期间不应修改元素。
这背后的原因是流应该促进并行执行。在这里,修改流的元素可能会导致意外行为。
结论
在本文中,我们看到了一些示例,它们显示了 Collection.forEach()和 Collection.stream().forEach()之间的细微差别。但是,重要的是要注意上面显示的所有示例仅仅是为了比较迭代集合的两种方式。
如果我们不需要流但只想迭代集合,则第一个选择应该直接在集合上使用 forEach()(Collection.forEach())。