关于java:Collection.stream()。forEach()和Collection.forEach()有什么区别?

What is difference between Collection.stream().forEach() and Collection.forEach()?

我了解使用.stream(),我可以使用像.filter()这样的链式操作,也可以使用并行流。 但是,如果我需要执行小的操作(例如,打印列表的元素),它们之间有什么区别?

1
2
collection.stream().forEach(System.out::println);
collection.forEach(System.out::println);

对于诸如所示的简单情况,它们基本上是相同的。但是,有许多细微的差别可能很重要。

一个问题是订购。对于Stream.forEach,顺序是不确定的。顺序流不太可能发生,但是,在Stream.forEach规范中可以按任意顺序执行。这确实在并行流中经常发生。相反,如果指定了Iterable.forEach,则总是按Iterable的迭代顺序执行。

另一个问题是副作用。 Stream.forEach中指定的操作必须是无干扰的。 (请参阅java.util.stream软件包文档。)Iterable.forEach可能具有较少的限制。对于java.util中的集合,Iterable.forEach通常将使用该集合的Iterator,其中大多数被设计为快速失败的,并且如果在迭代过程中对集合进行结构修改,则会抛出ConcurrentModificationException。但是,在迭代过程中允许进行非结构化的修改。例如,ArrayList类文档说"仅设置元素的值不是结构修改"。因此,允许ArrayList.forEach的操作没有问题地设置基础ArrayList中的值。

并发集合又一次不同。它们不是快速失败,而是设计为弱一致性。完整定义在该链接上。简要地,请考虑ConcurrentLinkedDeque。允许将传递给其forEach方法的操作修改底层双端队列,甚至在结构上也是如此,并且永远不会抛出ConcurrentModificationException。但是,发生的修改在此迭代中可能可见,也可能不可见。 (因此保持"弱"一致性。)

如果Iterable.forEach在同步集合上进行迭代,则仍然可以看到另一个差异。在这样的集合上,Iterable.forEach一次获取该集合的锁,并将其保存在对action方法的所有调用中。 Stream.forEach调用使用集合的分隔符,该分隔符不会锁定,并且依赖于流行的非干扰规则。支持该流的集合可以在迭代期间进行修改,如果是,则可能导致ConcurrentModificationException或不一致的行为。


该答案本身与循环的各种实现的性能有关。它与被称为"非常频繁"的循环(如数百万次调用)的边际相关。在大多数情况下,循环的内容将是迄今为止最昂贵的元素。对于确实经常循环的情况,这可能仍然很有趣。

您应该在目标系统下重复此测试,因为这是特定于实现的(完整的源代码)。

我在快速的Linux机器上运行openjdk版本1.8.0_111。

我编写了一个测试,该测试使用此代码针对integers(10 ^ 0-> 10 ^ 5条目)的不同大小在列表上循环10 ^ 6次。

结果如下,最快的方法取决于列表中条目的数量。

但是,即使在最糟糕的情况下,表现最差的人也要花10秒循环10 ^ 5个条目10 ^ 6次,因此实际上在所有情况下其他考虑因素都更为重要。

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
public int outside = 0;

private void forCounter(List<Integer> integers) {
    for(int ii = 0; ii < integers.size(); ii++) {
        Integer next = integers.get(ii);
        outside = next*next;
    }
}

private void forEach(List<Integer> integers) {
    for(Integer next : integers) {
        outside = next * next;
    }
}

private void iteratorForEach(List<Integer> integers) {
    integers.forEach((ii) -> {
        outside = ii*ii;
    });
}
private void iteratorStream(List<Integer> integers) {
    integers.stream().forEach((ii) -> {
        outside = ii*ii;
    });
}

这是我的时间安排:毫秒/功能/列表中的条目数。
每次运行为10 ^ 6循环。

1
2
3
4
5
                           1    10    100    1000    10000
         for with index   39   112    920    8577    89212
       iterator.forEach   27   116    959    8832    88958
               for:each   53   171   1262   11164   111005
iterable.stream.forEach  255   324   1030    8519    88419

如果您重复实验,我将发布完整的源代码。请编辑此答案,并在结果中加上已测试系统的注释。

使用MacBook Pro,2.5 GHz Intel Core i7、16 GB,macOS 10.12.6:

1
2
3
4
5
                           1    10    100    1000    10000
         for with index   49   145    887    7614    81130
       iterator.forEach   27   106   1047    8516    88044
               for:each   46   143   1182   10548   101925
iterable.stream.forEach  393   397   1108    8908    88361


您提到的两者之间没有区别,至少在概念上,Collection.forEach()只是一个简写。

内部的stream()版本由于对象创建而具有更多的开销,但是从运行时间来看,两者都没有开销。

两种实现都最终对collection内容进行一次迭代,并在迭代过程中打印出元素。