Java 8-转换列表的最佳方法:map或foreach?


188

我有一个列表myListToParse,我想在其中过滤元素并在每个元素上应用方法,然后将结果添加到另一个列表中myFinalList

使用Java 8,我注意到可以用2种不同的方式来做到这一点。我想知道它们之间更有效的方法,并理解为什么一种方法比另一种更好。

我愿意就第三种方式提出任何建议。

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

55
第二个。适当的功能应该没有副作用,在您的第一个实现中,您正在修改外部世界。
ThanksForAllTheFish 2015年

37
只是一个风格问题,但elt -> elt != null可以替换Objects::nonNull
the8472

2
@ the8472更好的办法是首先确保集合中没有空值,然后与Optional<T>结合使用flatMap
Herman 2015年

2
@SzymonRoziewski,不完全是。对于这样琐碎的事情,在后台设置并行流所需的工作将使使用此构造静音。
MK 2015年

2
请注意,您可以.map(this::doSomething)假设这doSomething是一种非静态方法来编写。如果是静态的,则可以替换this为类名。
Herman,2015年

Answers:


153

不必担心任何性能差异,在这种情况下,通常它们会很小。

方法2是可取的,因为

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

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

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


43

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

在性能方面,在开始使用并行流之前,它们似乎是等效的。在这种情况下,地图的性能会好得多。参见下面的微型基准测试结果:

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

1
关于性能,如果您使用parallelStreams,那么在您的情况下,“ map”确实胜过“ forEach”。我的基准时间以毫秒为单位:SO28319064.forEach:187,310±1,768 ms / op-SO28319064.map:189,180±1,692 ms / op --SO28319064.mapParallelStream:55,577±0,782 ms / op
Giuseppe Bertone

2
@GiuseppeBertone,由亚述决定,但据我看您的编辑与原作者的意图背道而驰。如果您想添加自己的答案,最好添加它,而不要编辑太多现有答案。现在,指向微基准的链接也与结果无关。
Tagir Valeev

5

使用流的主要好处之一是它提供了以声明性方式(即,使用编程的函数样式)处理数据的能力。它还免费提供了多线程功能,这意味着无需编写任何额外的多线程代码即可使您的流并发。

假设您正在探索这种编程风格的原因是,您想利用这些好处,那么您的第一个代码示例就可能无法正常工作,因为该foreach方法被归类为终端方法(这意味着它可能产生副作用)。

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

  1. 不干扰,表示该函数在不并发的情况下不应更改流的源(例如ArrayList)。
  2. 在进行并行处理时(避免由于线程调度差异而引起的)意外状态的无状态。

第二种方法的另一个好处是,如果流是并行的,并且收集器是并发且无序的,则这些特征可以为归约操作提供有用的提示,以便同时进行收集。


4

如果使用Eclipse Collections,则可以使用该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);

它渴望评估,并且应该比使用Stream更快。

注意:我是Eclipse Collections的提交者。


1

我喜欢第二种方式。

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

使用时toList,即使您使用并行流,Streams API也会保留顺序。


我不确定这是否是正确的建议:如果他想使用并行流,但仍可以保留顺序,他可以使用forEachOrdered而不是forEach。但是,作为forEach状态文档,保留相遇顺序会牺牲并行性的好处。我怀疑那时也是如此toList
Herman,2015年

0

没有流的原因stream().toArray()下,还有第三种选择-using -see注释有toList方法。事实证明它比forEach()或collect()慢,并且表现力较低。可能会在以后的JDK构建中对其进行优化,因此以防万一。

假设 List<String>

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

具有micro-micro基准,1M条目,20%的空值和doSomething()中的简单转换

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()在接收方ArrayList上失败并显示“ 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}

0

可能是方法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());

0

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

ListX myListToParse;

ListX myFinalList = myListToParse.filter(elt-> elt!= null).map(elt-> doSomething(elt));

在首次访问之前(以及在物化列表被缓存和重用之后),才会评估myFinalList。

[披露我是独眼巨人反应的主要开发者]

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.