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

Posted

技术标签:

【中文标题】如何将 Java8 流的元素添加到现有列表中【英文标题】:How to add elements of a Java8 stream into an existing List 【发布时间】:2014-05-10 07:55:58 【问题描述】:

Javadoc of Collector 展示了如何将流的元素收集到一个新的列表中。是否有一种方法可以将结果添加到现有的 ArrayList 中?

【问题讨论】:

已有答案here。查找“添加到现有的Collection”项 【参考方案1】:

注意: nosid's answer 展示了如何使用forEachOrdered() 添加到现有集合。这是改变现有集合的有用且有效的技术。我的回答说明了为什么您不应该使用 Collector 来改变现有集合。

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

原因是收集器被设计为支持并行性,即使在非线程安全的集合上也是如此。他们这样做的方式是让每个线程独立地操作自己的中间结果集合。每个线程获取自己的集合的方式是调用Collector.supplier(),每次都需要返回一个new集合。

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

来自Balder 和assylias 的几个答案建议使用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。这是因为多个线程在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]

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

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

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

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

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

好的,那么请务必记住在使用此代码之前在任何流上调用sequential()

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

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

别这样。

【讨论】:

很好的解释! - 感谢您澄清这一点。我将编辑我的答案,建议永远不要对可能的并行流执行此操作。 如果问题是,如果有一个单行器可以将流的元素添加到现有列表中,那么简短的回答是。看我的回答。但是,我同意你的观点,将 Collectors.toCollection() 与现有列表结合使用是错误的方式。 是的。我想我们其他人都在考虑收藏家。 很好的答案!即使您明确反对,我也很想使用顺序解决方案,因为如上所述它必须运行良好。但是 javadoc 要求 toCollection 方法的供应商参数每次都应该返回一个新的空集合这一事实说服我不要这样做。我真的很想打破核心 Java 类的 javadoc 契约。 @AlexCurvers 如果你想让流有副作用,你几乎肯定想使用forEachOrdered。副作用包括向现有集合添加元素,无论它是否已经有元素。如果要将流的元素放入 new 集合中,请使用 collect(Collectors.toList())toSet()toCollection()【参考方案2】:

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

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

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

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

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

【讨论】:

+1 有趣的是,有这么多人声称没有可能,但实际上是不可能的。顺便提一句。我在answer two month ago 中加入了forEach(existing::add) 作为一种可能性。我也应该添加forEachOrdered... 你有什么理由使用forEachOrdered而不是forEach @membersound: forEachOrdered 适用于 sequentialparallel 流。相反,forEach 可能会为并行流并发执行传递的函数对象。在这种情况下,函数对象必须正确同步,例如通过使用Vector&lt;Integer&gt; @BrianGoetz:我不得不承认,Stream.forEachOrdered 的文档有点不精确。但是,我看不出对这个规范的任何合理解释,其中target::add 的任何两个调用之间没有happens-before 关系。无论从哪个线程调用该方法,都没有data race。我本来希望你知道的。 就我而言,这是最有用的答案。它实际上展示了一种将项目从流中插入现有列表的实用方法,这就是问题所要求的(尽管有误导性的词“收集”)【参考方案3】:

简短的回答是否(或应该是否)。 编辑: 是的,有可能(请参阅下面的 assylias 的回答),但请继续阅读。 EDIT2:但请参阅 Stuart Marks 的回答,这是您仍然不应该这样做的另一个原因!

更长的答案:

Java 8 中这些结构的目的是为语言引入Functional Programming 的一些概念;在函数式编程中,通常不会修改数据结构,而是通过映射、过滤、折叠/归约等许多转换从旧结构中创建新的结构。

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

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

然后执行list.addAll(newList) — 再次:如果你真的必须这样做。

(或构造一个连接旧列表和新列表的新列表,并将其分配回list 变量——这比@987654326 更符合FP 精神一点 @)

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

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

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

【讨论】:

@assylias:逻辑上没有错,因为有 or 部分;无论如何,添加一个注释。 简短的回答是正确的。提出的单行代码在简单情况下会成功,但在一般情况下会失败。 较长的答案大部分是正确的,但 API 的设计主要是关于并行性,而不是关于函数式编程。虽然当然有很多关于 FP 的东西都适合并行性,所以这两个概念是一致的。 @StuartMarks:有趣:在哪些情况下 assylias 的答案中提供的解决方案会失效? (以及关于并行性的优点——我想我太急于提倡 FP) 整个答案都是错误的,不管是短还是长。 nosid 给出了正确的单行。因此,所有解释为什么不存在这样的单线都是毫无意义的。【参考方案4】:

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

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

但正如其他答案中指出的那样,永远不要这样做,尤其是当流可能是并行流时 - 使用风险自负...

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

【讨论】:

啊,真可惜:P 如果流并行运行,此技术将严重失败。 收集提供者有责任确保它不会失败 - 例如通过提供并发集合。 不,此代码违反了 toCollection() 的要求,即供应商返回一个新的、适当类型的空集合。即使目标是线程安全的,为并行情况进行的合并也会导致不正确的结果。 @Balder 我添加了一个应该澄清这一点的答案。【参考方案5】:

您只需将您的原始列表引用为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);
  

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

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

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

【讨论】:

我的意思是说如何添加/收集到现有列表中,而不仅仅是重新分配。 嗯,从技术上讲,你不能在函数式编程范式中做那种事情,流就是这样。在函数式编程中,状态不会被修改,而是在持久数据结构中创建新状态,使其对于并发目的是安全的,并且更具功能性。我提到的方法是您可以做的,或者您可以诉诸旧式的面向对象方法,在这种方法中迭代每个元素,并根据需要保留或删除元素。【参考方案6】:

我会将旧列表和新列表连接为流并将结果保存到目标列表。并行工作也很好。

我将使用 Stuart Marks 给出的接受答案示例:

List<String> destList = Arrays.asList("foo");
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");

destList = Stream.concat(destList.stream(), newList.stream()).parallel()
            .collect(Collectors.toList());
System.out.println(destList);

//output: [foo, 0, 1, 2, 3, 4, 5]

希望对你有帮助。

【讨论】:

【参考方案7】:

假设我们有现有列表,并将使用 java 8 进行此活动 `

import java.util.*;
import java.util.stream.Collectors;

public class AddingArray 

    public void addArrayInList()
        List<Integer> list = Arrays.asList(3, 7, 9);

   // And we have an array of Integer type 

        int nums[] = 4, 6, 7;

   //Now lets add them all in list
   // converting array to a list through stream and adding that list to previous list
        list.addAll(Arrays.stream(nums).map(num -> 
                                       num).boxed().collect(Collectors.toList()));
     

`

【讨论】:

【参考方案8】:

targetList = sourceList.stream().flatmap(List::stream).collect(Collectors.toList());

【讨论】:

以上是关于如何将 Java8 流的元素添加到现有列表中的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Java DocumentBuilder 中解析 XSD 文件期间将元素附加到现有节点列表

如何将所有列表元素添加到另一个列表中[重复]

如何在Dart中合并列表

列表,如何一次添加多个值

Python如何将列表中的元素添加到另一个字符串列表中

如何将元素添加到列表中?