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

cod*_*efx 139 java java-8 java-stream collectors

收集器的Javadoc显示如何将流的元素收集到新的List中.是否有一个单行程序将结果添加到现有的ArrayList中?

Stu*_*rks 175

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

简单的答案是没有,至少,不是一般的,你不应该使用Collector修改现有的集合.

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

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

来自Balderassylias的几个回答建议使用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);
Run Code Online (Sandbox Code Playgroud)

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

List<String> destList =
    Collections.synchronizedList(new ArrayList<>(Arrays.asList("foo")));
Run Code Online (Sandbox Code Playgroud)

这将不再失败,但有例外.但不是预期的结果:

[foo, 0, 1, 2, 3]
Run Code Online (Sandbox Code Playgroud)

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

[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]
Run Code Online (Sandbox Code Playgroud)

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

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

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

stream.collect(Collectors.toCollection(() -> existingList))
Run Code Online (Sandbox Code Playgroud)

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

好的,那么只需确保sequential()在使用此代码之前记住调用任何流:

stream.sequential().collect(Collectors.toCollection(() -> existingList))
Run Code Online (Sandbox Code Playgroud)

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

不要这样做.

  • 很好的解释!- 感谢您澄清这一点。我将编辑我的答案,建议永远不要对可能的并行流执行此操作。 (3认同)
  • 如果问题是,如果有一个单行将流的元素添加到现有列表中,那么简短的答案是_yes_.看我的回答.但是,我同意你的看法,将_Collectors.toCollection()_与现有列表结合使用是错误的. (2认同)

nos*_*sid 151

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

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

source.stream()
      .map(String::length)
      .forEachOrdered(target::add);
Run Code Online (Sandbox Code Playgroud)

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

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

  • +1有趣的是,有这么多人声称没有可能存在.顺便说一句.我把[forEach(existing :: add)`作为一种可能性包含在[两个月前的答案]中(http://stackoverflow.com/questions/21522341/collection-to-stream-to-a-new-collection/21526973 #21526973).我应该添加`forEachOrdered` ... (6认同)
  • @membersound:`forEachOrdered`适用于_sequential_和_parallel_流.相反,`forEach`可以并行地为并行流执行传递的函数对象.在这种情况下,函数对象必须正确同步,例如使用`Vector <Integer>`. (6认同)
  • 有没有理由你使用`forEachOrdered`而不是`forEach`? (5认同)
  • +1这是这个确切问题的实际答案 (2认同)
  • 就我而言,这是最有用的答案。它实际上展示了一种从流中将项目插入到现有列表中的实用方法,这就是问题所要求的(尽管有误导性的词“收集”) (2认同)

Eri*_*lun 11

简短的回答是否定的(或者应该是否定的).编辑:是的,这是可能的(见下面的assylias'答案),但继续阅读.编辑2:但看到斯图尔特马克斯的回答是另一个原因,你仍然不应该这样做!

答案越长:

Java 8中这些结构的目的是向该语言引入一些函数式编程概念; 在功能编程中,数据结构通常不会被修改,相反,新的数据结构是通过转换(如map,filter,fold/reduce等)来创建的.

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

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

然后再做list.addAll(newList)- 如果你真的必须.

(或构建一个新的列表拼接旧与新的一个,并将其分配回list变,这是一个有点更FP比灵addAll)

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

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

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

  • 整个答案无论长短都是错误的。[nosid](http://stackoverflow.com/a/22755225/2711488) 给出了正确的一句话。因此,所有解释为什么这样的俏皮话不存在都是毫无意义的。 (2认同)

Bal*_*der 10

Erik Allik已经给出了很好的理由,为什么你很可能不想将流的元素收集到现有的List中.

无论如何,如果你真的需要这个功能,你可以使用以下单行.

编辑:但正如斯图尔特马克斯在他的回答中解释的那样,如果流可能是并行流,你应该永远不要这样做 - 使用风险自负......

list.stream().collect(Collectors.toCollection(() -> myExistingList));
Run Code Online (Sandbox Code Playgroud)

  • 如果流并行运行,这种技术将会非常失败. (2认同)
  • 不,此代码违反了toCollection()的要求,即供应商返回适当类型的新的空集合.即使目标是线程安全的,对并行情况进行的合并也会导致错误的结果. (2认同)