如何将Java8stream的元素添加到现有列表中

收集器的Javadoc显示如何将stream的元素收集到新列表中。 有没有一个单线程将结果添加到现有的ArrayList中?

注意: nosid的答案显示了如何使用forEachOrdered()添加到现有的集合。 这是改变现有集合的有用和有效的技术。 我的回答解决了为什么你不应该使用Collector来改变现有的集合。

简短的回答是,至less,不是一般的,你不应该使用Collector来修改现有的集合。

原因是收集器被devise为支持并行性,甚至是不是线程安全的集合。 他们这样做的方式是让每个线程独立运行,并自行收集中间结果。 每个线程获取自己的集合的方式是调用每次返回集合所需的Collector.supplier()

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

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

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

我们举一个简单的例子:

 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 。 这是因为multithreading正在ArrayList上运行,这是一个线程不安全的数据结构。 好的,我们让它同步:

 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] 

这是我上面描述的线程限制累加/合并操作的结果。 通过并行stream,每个线程都会调用供应商来获取自己的集合以进行中间累加。 如果您传递了返回相同集合的供应商,则每个线程都会将其结果附加到该集合。 由于线程之间没有sorting,所以结果将以任意顺序附加。

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

你可能会说,“我会确保按顺序运行我的stream”,然后继续写这样的代码

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

无论如何。 我build议不要这样做。 如果你控制stream,当然,你可以保证它不会并行运行。 我期望一种编程风格将出现在stream交付而不是集合的地方。 如果有人给你一个stream,并使用这个代码,如果stream恰好平行,它将失败。 更糟糕的是,有人可能会给你一个顺序stream,这段代码将正常工作一段时间,通过所有testing等。然后,一些任意时间后,系统中其他地方的代码可能会改变使用并行stream,这将导致您的代码打破。

OK,那么在使用这个代码之前,请确保记得在任何stream上调用sequential()

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

当然,你会记得每次都这样做,对吧? :-)假设你这样做。 然后,性能团队将想知道为什么他们精心制作的并行实现不提供任何加速。 再一次,他们会把它跟踪到你的代码,它迫使整个stream顺序运行。

不要这样做。

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

 List<String> source = ...; List<Integer> target = ...; source.stream() .map(String::length) .forEachOrdered(target::add); 

唯一的限制是, 目标是不同的列表,因为只要处理stream,就不允许更改stream的源。

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

简短的答案是否定的(或不应该是)。 编辑:是的,这是可能的(见下文assylias的答案),但继续阅读。 编辑2:但见Stuart标记的答案为什么你仍然不应该这样做的另一个原因!

更长的答案:

Java 8中这些构造的目的是向语言介绍函数式编程的一些概念; 在函数式编程中,通常不会修改数据结构,而是通过诸如map,filter,fold / reduce等转换来创build新的数据结构。

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

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

然后做list.addAll(newList) – 再次:如果你真的必须。

(或者构build一个连接旧的和新的列表的新列表,并将其分配回listvariables – 这比addAll的精神更多一点

至于API:即使API允许(再次看到assylias的答案),你应该尽量避免这样做,至less在一般情况下。 最好不要与范式(FP)作斗争,并试图学习而不是与之对抗(即使Java通常不是FP语言),只有在绝对需要时才采用“更脏”的策略。

真的很长的答案:(即如果你包括实际发现和阅读FP介绍/书的努力build议)

要找出为什么修改现有的列表通常是一个坏主意,并导致代码维护性较差 – 除非你修改一个局部variables,而且你的algorithm很短和/或微不足道,这超出了代码可维护性问题的范围 – find一个很好的介绍函数式编程(有数百),并开始阅读。 一个“预览”的解释是这样的:它在math上更合理,更容易理解不修改数据(在你的程序的大部分部分),并导致更高的水平和更less的技术(以及更人性化,一旦你的大脑从旧式的命令式思维转向)程序逻辑的定义。

Erik Allik已经给出了很好的理由,为什么你很可能不想收集一个stream的元素到现有的列表。

无论如何,如果你确实需要这个function,你可以使用下面的一行代码。

编辑:但正如斯图尔特·马克斯在他的回答中所解释的那样,如果这些stream可能是平行的stream,那么你不应该这样做 – 使用风险自负。

 list.stream().collect(Collectors.toCollection(() -> myExistingList)); 

您只需将您的原始列表引用到Collectors.toList()返回的列表。

这是一个演示:

 import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class Reference { public static void main(String[] args) { List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); System.out.println(list); // Just collect even numbers and start referring the new list as the original one. list = list.stream() .filter(n -> n % 2 == 0) .collect(Collectors.toList()); System.out.println(list); } } 

以下是如何将新创build的元素添加到原始列表中的一行。

 List<Integer> list = ...; // add even numbers from the list to the list again. list.addAll(list.stream() .filter(n -> n % 2 == 0) .collect(Collectors.toList()) ); 

这就是这个函数编程范式提供的。