在java中有多种方式对集合进行遍历。本教程中将看两个类似的方法 Collection.stream().forEach()和Collection.forEach()。
在大多数情况下,两者都会产生相同的结果,但是,我们会看到一些微妙的差异。
首先,创建一个迭代列表:
1 |
List |
最直接的方法是使用增强的for循环:
1 2 3 |
for(String s : list) { //do something with s } |
如果我们想使用函数式Java,我们也可以使用forEach()。我们可以直接在集合上这样做:
1 2 |
Consumer list.forEach(consumer); |
或者,我们可以在集合的流上调用forEach():
1 |
list.stream().forEach(consumer); |
两个版本都将迭代列表并打印所有元素:
1 |
ABCD ABCD |
在这个简单的例子中,我们使用的forEach()没有区别。
Collection.forEach()使用集合的迭代器(如果指定了一个),集合里元素的处理顺序是明确的。相反,Collection.stream().forEach()的处理顺序是不明确的。
在大多数情况下,我们选择上述两种方式的哪一种是没有区别的。但是有时候有。
并发流允许我们在多个线程中执行stream,在这种情况下,执行顺序也不明确的。Java只需要在调用任何最终操作(例如Collectors.toList())之前完成所有线程。
看一个例子,首先直接在集合上调用forEach(),然后在并发流上调用:
1 2 3 |
list.forEach(System.out::print); System.out.print(" "); list.parallelStream().forEach(System.out::print); |
如果我们多次运行代码,我们会看到list.forEach()以插入顺序处理元素,而 list.parallelStream().forEach()在每次运行会产生不同的结果。
一个可能的输出是:
1 |
ABCD CDBA |
另一个是:
1 |
ABCD DBCA |
让我们使用自定义迭代器定义一个列表,以反向顺序迭代集合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class ReverseList extends ArrayList
@Override public Iterator
int startIndex = this.size() - 1; List
Iterator
private int currentIndex = startIndex;
@Override public boolean hasNext() { return currentIndex >= 0; }
@Override public String next() { String next = list.get(currentIndex); currentIndex--; return next; }
@Override public void remove() { throw new UnsupportedOperationException(); } }; return it; } } |
当我们遍历列表时,再次使用forEach()直接在集合上,然后在流上:
1 2 3 4 5 6 |
List myList.addAll(list);
myList.forEach(System.out::print); System.out.print(" "); myList.stream().forEach(System.out::print); |
我们得到不同的结果:
1 |
DCBA ABCD |
结果不同的原因是在列表中使用的forEach()会使用自定义迭代器,而stream().forEach()只是从列表中逐个获取元素,会忽略迭代器。
很多集合在遍历的时候,不应该在结构上被修改(比如ArrayList或HashSet)。如果在迭代期间删除或添加元素,会抛出ConcurrentModification异常。
此外,集合设计为快速失败(fail-fast),这意味着一旦修改就抛出异常。
类似地,当我们在stream的执行期间添加或删除元素时,我们将得到ConcurrentModification异常。但是,异常将在稍后抛出。
两个forEach()方法之间的另一个细微差别是Java明确允许使用迭代器修改元素。相反,stream不能。
来看一下更详细的例子。
定义一个列表,删除最后一个元素(“D”):
1 2 3 4 5 6 |
Consumer System.out.println(s + " " + list.size()); if (s != null && s.equals("A")) { list.remove("D"); } }; |
遍历列表时,在打印第一个元素(“A”)后删除最后一个元素:
1 |
list.forEach(removeElement); |
因为forEach()是快速失败的,所以我们停止迭代并在处理下一个元素之前看到异常:
1 2 3 4 |
A 4 Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList.forEach(ArrayList.java:1252) at ReverseList.main(ReverseList.java:1) |
让我们看看如果我们使用stream().forEach()会发生什么:
1 |
list.stream().forEach(removeElement); |
在这里,我们继续迭代整个列表,然后才看到异常:
1 2 3 4 5 6 7 8 |
A 4 B 3 C 3 null 3 Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1380) at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580) at ReverseList.main(ReverseList.java:1) |
但是,Java并不保证会抛出ConcurrentModificationException。这意味着我们永远不应该编写依赖于此异常的程序。
我们可以在迭代列表时更改元素:
1 2 3 |
list.forEach(e -> { list.set(3, "E"); }); |
但是,虽然使用Collection.forEach()或stream()。forEach()执行此操作没有问题,但Java要求对流的操作是无干扰的。这意味着在执行流管道期间不应修改元素。
这背后的原因是流应该促进并行执行。在这里,修改流的元素可能会导致意外行为。
在本文中,我们看到了一些示例,它们显示了Collection.forEach()和Collection.stream().forEach()之间的细微差别。
但是,重要的是要注意上面显示的所有示例仅仅是为了比较迭代集合的两种方式。
如果我们不需要流但只想迭代集合,则第一个选择应该直接在集合上使用forEach()。
GitHub上提供了本文中示例的源代码。
编译:https://www.baeldung.com/java-collection-stream-foreach
更多教程:
黑客日教程