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

2022-08-31 06:17:49

Collector 的 Javadoc 展示了如何将流的元素收集到新的 List 中。是否有单行代码将结果添加到现有的 ArrayList 中?


答案 1

注意:nosid 的答案显示了如何使用 forEachOrdered() 添加到现有集合中。这是用于变异现有集合的有用且有效的技术。我的回答解释了为什么不应该使用收集器来改变现有集合。

简短的答案是否定的,至少不是一般情况下,您不应该使用 a 来修改现有集合。Collector

原因是收集器旨在支持并行性,即使对于线程不安全的集合也是如此。他们这样做的方法是让每个线程在自己的中间结果集合上独立运行。每个线程获取自己的集合的方式是调用每次返回集合所需的 the。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);

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

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 实现可能具有完全不同的行为。无论如何,这解释了目标中的奇怪结果和重复元素。List.addAll()ArrayList.addAll()

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

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

无论如何。我建议不要这样做。当然,如果您控制了流,则可以保证它不会并行运行。我预计会出现一种编程风格,流被传递而不是集合。如果有人递给你一个流,而你使用了这段代码,那么如果流恰好是并行的,它就会失败。更糟糕的是,有人可能会给你一个顺序流,这个代码会在一段时间内正常工作,通过所有测试,等等。然后,经过任意时间后,系统中其他位置的代码可能会更改为使用并行流,这将导致代码中断。

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

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

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

别这样。


答案 2

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

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

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

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

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


推荐