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

问题

我有一个listmyListToParse其中我想过滤元素并在每个元素上应用一个方法,并将结果添加到另一个listmyFinalList

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

#1 热门回答(109 赞)

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

方法2是优选的,因为

  • 它不需要改变lambda表达式之外的集合,
  • 它更具可读性,因为在集合管道中执行的不同步骤是按顺序写入的(首先是过滤操作,然后是 Map 操作,然后收集结果),(有关收集管道的好处的更多信息,请参阅Martin Fowler的优秀信息)文章)
  • 你可以通过替换使用的收集器轻松更改值的收集方式。在某些情况下,你可能需要编写自己的收集器,但好处是你可以轻松地重用它。

#2 热门回答(31 赞)

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

性能方面,在你开始使用并行流之前,它们似乎是等效的。在那种情况下,** Map **将会表现得更好。见下面的micro benchmark结果:

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。 Butthat is really a bad idea if you are using parallel streams

最后请注意,你的第二个代码段可以使用方法引用和静态导入以更简洁的方式编写:

myFinalList = myListToParse.stream()
    .filter(Objects::nonNull)
    .map(this::doSomething)
    .collect(toList());

#3 热门回答(5 赞)

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

假设你正在探索这种编程风格的原因是你希望利用这些优势,那么你的第一个代码示例可能无法正常运行,因为foreach方法被归类为终端(意味着它可能产生副作用)。

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

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

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