如何将Java8流的元素添加到现有List中

问题

Javadoc of Collector显示如何将流的元素收集到新的List中。是否存在将结果添加到现有ArrayList中的单行程序?


#1 热门回答(125 赞)

据我所知,到目前为止所有其他答案都使用了一个收集器来向现有流添加元素。但是,有一个更短的解决方案,它适用于顺序和并行流。你可以简单地将methodforEachOrderedin与方法引用结合使用。

List<String> source = ...;
List<Integer> target = ...;

source.stream()
      .map(String::length)
      .forEachOrdered(target::add);

唯一的限制是,源和目标是不同的列表,因为只要处理了流,就不允许对流的源进行更改。

请注意,此解决方案适用于顺序和并行流。但是,它并没有从并发中受益。传递给toforEachOrdered的方法引用将始终按顺序执行。


#2 热门回答(121 赞)

注意:nosid's answer显示如何使用forEachOrdered()添加到现有集合。这是一种用于改变现有集合的有用且有效的技术。我的回答解决了为什么你不应该使用aCollector改变现有的集合。

简短的回答是no,至少在一般情况下,你不应该使用aCollector来修改现有的集合。

原因是收集器旨在支持并行性,即使是非线程安全的集合也是如此。他们这样做的方法是让每个线程独立地在其自己的中间结果集合上运行。每个线程获得自己的集合的方式是调用Collector.supplier(),每次返回a新的集合。

然后,这些中间结果集合再次以线程限制的方式合并,直到存在单个结果集合。这是collect()操作的最终结果。

一对夫妇的答案来自Balder和416600653,建议使用Collectors.toCollection()然后传递一个返回现有列表而不是新列表的供应商。这违反了供应商的要求,即每次都返回一个新的空集合。

这将适用于简单的案例,如其答案中的示例所示。但是,它会失败,特别是如果流并行运行。 (即使在连续的情况下,库的未来版本也可能以某种无法预料的方式发生变化,导致其失败。)

我们举一个简单的例子:

List<String> destList = new ArrayList<>(Arrays.asList("foo"));
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
newList.parallelStream()
       .collect(Collectors.toCollection(() -> destList));
System.out.println(destList);

当我运行这个程序时,我经常得到一个ArrayIndexOutOfBoundsException。这是因为多个线程在3775794007上运行,这是一个不安全的数据结构。好的,让它同步:

List<String> destList =
    Collections.synchronizedList(new ArrayList<>(Arrays.asList("foo")));

除了例外,这将不再失败。但不是预期的结果:

[foo, 0, 1, 2, 3]

它给出了这样奇怪的结果:

[foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0]

这是我在上面描述的线程限制的累积/合并操作的结果。使用并行流,每个线程调用供应商以获得其自己的集合以进行中间累积。如果你通过了返回相同收集的供应商,则每个线程会将其结果附加到该集合。由于线程之间没有排序,因此结果将以某种任意顺序附加。

然后,当合并这些中间集合时,这基本上将列表与其自身合并。使用List.addAll()合并列表,如果在操作期间修改了源集合,则表示结果未定义。在这种情况下,ArrayList.addAll()进行数组复制操作,因此它最终会复制自身,这是人们所期望的那种,我猜。 (请注意,其他List实现可能具有完全不同的行为。)无论如何,这解释了目标中的奇怪结果和重复元素。

你可能会说,"我只是确保按顺序运行我的流"并继续编写这样的代码

stream.collect(Collectors.toCollection(() -> existingList))

无论如何。我建议不要这样做。如果你控制流,当然,你可以保证它不会并行运行。我希望在流式传输而不是集合的情况下会出现一种编程风格。如果有人给你一个流并且你使用这个代码,那么如果流恰好是并行的话它将会失败。更糟糕的是,有人可能会给你一个顺序流,这段代码可以正常运行一段时间,通过所有测试等等。然后,一些任意的时间后,系统中其他地方的代码可能会改变为使用并行流,这将导致你的**代码打破。

好的,那么请确保在使用此代码之前记得在任何流上调用sequential()

stream.sequential().collect(Collectors.toCollection(() -> existingList))

当然,你会记得每次都这样做,对吗? :-)假设你这样做。然后,性能团队将会想知道为什么他们所有精心设计的并行实现都没有提供任何加速。再一次,他们会将它追溯到你的代码,这迫使整个流顺序运行。

不要这样做。


#3 热门回答(11 赞)

简答题是否(或应该是否).编辑:,这是可能的(见下面的assylias'答案),但请继续阅读.**EDIT2:**但是看到Stuart Marks的回答是另一个原因,你仍然不应该'做到了!
更长的答案:
Java 8中这些结构的目的是将Functional Programming的一些概念引入该语言;在功能编程中,通常不会修改数据结构,而是通过诸如map,filter,fold / reduce等许多其他变换来创建新的数据结构。

如果你要修改旧列表,只需将映射的项目收集到一个新的列表中:

final List<Integer> newList = list.stream()
                                  .filter(n -> n % 2 == 0)
                                  .collect(Collectors.toList());

然后dolist.addAll(newList)-再次:如果你真的必须。

(或构建一个连接旧的和新的列表的新列表,并将其分配回到list变量 - 这在FP的精神中比在addAll中更少一点)

至于API:即使API允许它(再次,请参阅assylias的回答),你应该尽量避免这样做,至少在一般情况下如此。最好不要对抗范式(FP)并尝试学习它而不是对抗它(即使Java通常不是FP语言),并且只有在绝对需要时才采用"更脏"的策略。

真正很长的答案:(即如果你包括按照建议实际查找和阅读FP介绍/书籍的努力)

要找出为什么修改现有列表通常是一个坏主意并导致可维护性较低的代码 - 除非你修改局部变量并且算法很短和/或微不足道,这超出了代码可维护性问题的范围 - 找到功能编程的好介绍(有数百种)并开始阅读。 "预览"解释类似于:它在数学上更合理,更容易推理不修改数据(在程序的大多数部分中)并导致更高级别和更少技术(以及更人性化,一旦你的大脑从旧式命令式思维过渡到程序逻辑的定义。