我有一个列表myListToParse,我想在其中过滤元素并对每个元素应用一个方法,并将结果添加到另一个列表myFinalList中。
在Java 8中,我注意到我可以用两种不同的方式来做到这一点。我想知道他们之间更有效的方式,并了解为什么一种方式比另一种更好。
我愿意接受任何关于第三条路的建议。
方法1:
myFinalList = new ArrayList<>();
myListToParse.stream()
.filter(elt -> elt != null)
.forEach(elt -> myFinalList.add(doSomething(elt)));
方法2:
myFinalList = myListToParse.stream()
.filter(elt -> elt != null)
.map(elt -> doSomething(elt))
.collect(Collectors.toList());
使用流的主要好处之一是,它提供了以声明式方式处理数据的能力,也就是说,使用函数式编程风格。它还提供了免费的多线程功能,这意味着不需要编写任何额外的多线程代码来使流并发。
假设您探索这种编程风格的原因是希望利用这些优点,那么您的第一个代码示例可能不是功能性的,因为foreach方法被归类为终端方法(这意味着它可能产生副作用)。
从函数式编程的角度来看,第二种方法是首选的,因为map函数可以接受无状态lambda函数。更明确地说,传递给map函数的lambda应该是
非干扰,意味着函数不应该改变流的源,如果它是非并发的(例如ArrayList)。
无状态以避免在进行并行处理时出现意外结果(由线程调度差异引起)。
第二种方法的另一个好处是,如果流是并行的,而收集器是并发的和无序的,那么这些特征可以为简化操作提供有用的提示,以便并发地进行收集。
我同意现有的答案,第二种形式更好,因为它没有任何副作用,更容易并行化(只需使用并行流)。
在性能方面,它们似乎是等效的,直到您开始使用并行流。在这种情况下,map会表现得更好。以下为微基准测试结果:
Benchmark Mode Samples Score Error Units
SO28319064.forEach avgt 100 187.310 ± 1.768 ms/op
SO28319064.map avgt 100 189.180 ± 1.692 ms/op
SO28319064.mapWithParallelStream avgt 100 55,577 ± 0,782 ms/op
你不能以同样的方式提升第一个例子,因为forEach是一个终端方法——它返回void——所以你不得不使用一个有状态的lambda。但如果你使用并行流,这真的是一个坏主意。
最后注意,你的第二个代码片段可以使用方法引用和静态导入以一种更简洁的方式编写:
myFinalList = myListToParse.stream()
.filter(Objects::nonNull)
.map(this::doSomething)
.collect(toList());
不要担心任何性能差异,在这种情况下,它们通常是最小的。
方法2更可取,因为
it doesn't require mutating a collection that exists outside the lambda expression.
it's more readable because the different steps that are performed in the collection pipeline are written sequentially: first a filter operation, then a map operation, then collecting the result (for more info on the benefits of collection pipelines, see Martin Fowler's excellent article.)
you can easily change the way values are collected by replacing the Collector that is used. In some cases you may need to write your own Collector, but then the benefit is that you can easily reuse that.