Java 8 - 转化列表的最佳方式:map 还是 foreach ?

我有一个列表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());
218601 次浏览

我更喜欢第二种方式。

当使用第一种方法时,如果你决定使用并行流来提高性能,你将无法控制forEach将元素添加到输出列表中的顺序。

当你使用toList时,Streams API将保留顺序,即使你使用并行流。

不要担心任何性能差异,在这种情况下,它们通常是最小的。

方法2更可取,因为

  1. 它不需要改变lambda表达式之外的集合。

  2. 它更具有可读性,因为在收集管道中执行的不同步骤是按顺序写入的:首先是过滤器操作,然后是映射操作,然后收集结果(有关收集管道的好处的更多信息,请参阅Martin Fowler的优秀的文章)。

  3. 你可以通过替换所使用的Collector来轻松改变值的收集方式。在某些情况下,你可能需要编写自己的Collector,但这样做的好处是你可以很容易地重用它。

我同意现有的答案,第二种形式更好,因为它没有任何副作用,更容易并行化(只需使用并行流)。

在性能方面,它们似乎是等效的,直到您开始使用并行流。在这种情况下,地图将执行得更好。下面是微基准的结果:

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());

使用流的主要好处之一是,它提供了以声明式方式处理数据的能力,也就是说,使用函数式编程风格。它还提供了免费的多线程功能,这意味着不需要编写任何额外的多线程代码来使流并发。

假设你探索这种编程风格的原因是你想利用这些好处,那么你的第一个代码样本可能不是功能性的,因为foreach方法被归类为终端(意味着它可能产生副作用)。

从函数式编程的角度来看,第二种方法是首选的,因为map函数可以接受无状态lambda函数。更明确地说,传递给map函数的lambda应该是

  1. 非干扰,意味着函数不应该改变流的源,如果它是非并发的(例如ArrayList)。
  2. 无状态以避免在进行并行处理时出现意外结果(由线程调度差异引起)。

第二种方法的另一个好处是,如果流是并行的,而收集器是并发的和无序的,那么这些特征可以为简化操作提供有用的提示,以便并发地进行收集。

如果你使用Eclipse集合,你可以使用collectIf()方法。

MutableList<Integer> source =
Lists.mutable.with(1, null, 2, null, 3, null, 4, null, 5);


MutableList<String> result = source.collectIf(Objects::nonNull, String::valueOf);


Assert.assertEquals(Lists.immutable.with("1", "2", "3", "4", "5"), result);

它会快速计算,应该比使用流快一点。

注意:我是Eclipse集合的提交者。

还有第三个选项——使用stream().toArray()——参见为什么stream没有一个toList方法下面的注释。它比forEach()或collect()慢,表达能力也更差。它可能会在以后的JDK构建中得到优化,所以在这里添加它以防万一。

假设List<String>

    myFinalList = Arrays.asList(
myListToParse.stream()
.filter(Objects::nonNull)
.map(this::doSomething)
.toArray(String[]::new)
);

在doSomething()中有一个微型基准测试,1M个条目,20%的空值和简单的转换

private LongSummaryStatistics benchmark(final String testName, final Runnable methodToTest, int samples) {
long[] timing = new long[samples];
for (int i = 0; i < samples; i++) {
long start = System.currentTimeMillis();
methodToTest.run();
timing[i] = System.currentTimeMillis() - start;
}
final LongSummaryStatistics stats = Arrays.stream(timing).summaryStatistics();
System.out.println(testName + ": " + stats);
return stats;
}

结果如下

并行:

toArray: LongSummaryStatistics{count=10, sum=3721, min=321, average=372,100000, max=535}
forEach: LongSummaryStatistics{count=10, sum=3502, min=249, average=350,200000, max=389}
collect: LongSummaryStatistics{count=10, sum=3325, min=265, average=332,500000, max=368}

顺序:

toArray: LongSummaryStatistics{count=10, sum=5493, min=517, average=549,300000, max=569}
forEach: LongSummaryStatistics{count=10, sum=5316, min=427, average=531,600000, max=571}
collect: LongSummaryStatistics{count=10, sum=5380, min=444, average=538,000000, max=557}

并行,没有空值和过滤器(因此流是SIZED): toArrays在这种情况下具有最好的性能,并且.forEach()在接收数组列表上的"indexOutOfBounds"失败,必须替换为.forEachOrdered()

toArray: LongSummaryStatistics{count=100, sum=75566, min=707, average=755,660000, max=1107}
forEach: LongSummaryStatistics{count=100, sum=115802, min=992, average=1158,020000, max=1254}
collect: LongSummaryStatistics{count=100, sum=88415, min=732, average=884,150000, max=1014}

可能是方法3。

我总是喜欢把逻辑分开。

Predicate<Long> greaterThan100 = new Predicate<Long>() {
@Override
public boolean test(Long currentParameter) {
return currentParameter > 100;
}
};
        

List<Long> sourceLongList = Arrays.asList(1L, 10L, 50L, 80L, 100L, 120L, 133L, 333L);
List<Long> resultList = sourceLongList.parallelStream().filter(greaterThan100).collect(Collectors.toList());

如果使用3rd Pary库是可以的,cyclops-react定义了内置此功能的Lazy扩展集合。例如,我们可以简单地写

ListX myListToParse;

ListX myFinalList = myListToParse。过滤(elt -> elt != null) .map(elt -> doSomething(elt));< / p >

myFinalList在第一次访问之前(在物化列表被缓存和重用之后)不会被求值。

[披露我是cyclops-react的主要开发者]