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


Answers:


197

注意: nosid的答案显示了如何使用来添加到现有集合forEachOrdered()。这是对现有集合进行变异的有用且有效的技术。我的答案解决了为什么您不应该使用A Collector来突变现有集合的原因。

简短的答案是no,至少在一般情况下不是这样,您不应该使用a 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);

当我运行该程序时,通常会收到一个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]

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

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

您可能会说:“我将确保按顺序运行流”并继续编写这样的代码

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

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

确定,然后确保sequential()在使用此代码之前记得记得在任何流上调用:

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

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

不要这样


很好的解释!-感谢您澄清这一点。我将编辑答案,建议不要对可能的并行流执行此操作。
巴尔德2014年

3
如果问题是,如果存在将流中的元素添加到现有列表中的单一方法,那么简短的答案是yes。看我的答案。但是,我同意你的观点,将Collectors.toCollection()与现有列表结合使用是错误的方法。
nosid 2014年

真正。我想我们其余的人都在思考收藏家。
斯图尔特(Stuart Marks)

好答案!我非常想使用顺序解决方案,即使您明确建议不要这样做,因为如上所述,它必须能很好地工作。但是事实是javadoc要求toCollection每次都说服我不要这样做时,该方法的provider参数应返回一个新的空集合。我真的想打破核心Java类的javadoc契约。
2016年

1
@AlexCurvers如果您希望流具有副作用,则几乎可以肯定要使用forEachOrdered。副作用包括向现有集合中添加元素,无论它是否已经具有元素。如果你想有一个流的元素放入一个新的收集,使用collect(Collectors.toList())toSet()toCollection()
斯图尔特·马克

169

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

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

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

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

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


6
+1有趣的是,有这么多人声称只有一种情况才有可能。顺便说一句。我forEach(existing::add)两个月前回答中加入了一种可能性。我也应该添加forEachOrdered……
Holger 2014年

5
有什么理由forEachOrdered代替您使用forEach
–membersound

6
@membersound:forEachOrdered适用于顺序流和并行流。相反,forEach对于并行流,可能会同时执行传递的函数对象。在这种情况下,必须正确同步功能对象,例如通过使用Vector<Integer>
2015年

@BrianGoetz:我必须承认,Stream.forEachOrdered的文档有点不精确。但是,我看不到对此规范的任何合理解释,在该规范中,任何两个调用之间都没有发生先于关系target::add。无论从哪个线程调用该方法,都不会发生数据争用。我希望您知道这一点。
nosid

就我而言,这是最有用的答案。它实际上显示了一种从流中将项目插入到现有列表中的实用方法,这就是问题的要求(尽管有误导性的“收集”一词)
Wheezil,

12

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

更长的答案:

Java 8中这些构造的目的是向语言引入一些函数式编程的概念。在函数式编程中,通常不修改数据结构,而是通过转换(例如映射,过滤器,折叠/缩小等)在旧数据结构的基础上创建新数据结构。

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

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

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

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

关于API:即使API允许这样做(同样,请参见assylias的答案),您也应至少避免这样做,至少通常是这样。最好不要对抗范式(FP)并尝试学习它而不是对抗它(即使Java通常不是FP语言),并且只有在绝对需要时才诉诸“较脏”的策略。

答案很长:(即,如果您包括按照建议实际查找和阅读FP简介/书籍的工作)

要找出为什么修改现有列表通常是一个坏主意,并且导致代码的可维护性较差-除非您要修改局部变量并且算法简短和/或琐碎,否则这超出了代码可维护性问题的范围-对函数式编程(有成百上千种)进行了很好的介绍,并开始阅读。“预览”解释将类似于:在数学上更合理,更容易推理出不修改数据(在程序的大多数部分中),并导致更高的水平和更少的技术性(一旦您的大脑也更人性化)脱离了程序逻辑的老式命令式定义)。


@assylias:从逻辑上讲,这没错,因为存在“ 或”部分;无论如何,添加了一个注释。
埃里克·卡普伦

1
简短的答案是正确的。提议的单线方案在简单情况下会成功,但在一般情况下会失败。
斯图尔特(Stuart Marks)2014年

较长的答案基本上是正确的,但是API的设计主要是关于并行性的,而很少涉及函数式编程。尽管FP当然有很多适合并行的东西,所以这两个概念很好地结合了起来。
斯图尔特(Stuart Marks)

@StuartMarks:有趣的是:在什么情况下,亚述答案中提供的解决方案会崩溃?(以及有关并行性的要点,我想我太急于主张FP)
Erik Kaplun 2014年

@ErikAllik我添加了涵盖此问题的答案。
斯图尔特(Stuart Marks)

11

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

无论如何,如果确实需要此功能,则可以使用以下单线。

但是正如Stuart Marks在他的回答中所解释的那样,如果流可能是并行流,则绝对不要这样做-使用后果自负...

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

嗯,太可惜了:P
Erik Kaplun

2
如果流并行运行,此技术将严重失败。
斯图尔特(Stuart Marks)

1
收集提供者有责任确保它不会失败-例如,通过提供并发收集。
2014年

2
不,此代码违反了toCollection()的要求,即供应商返回适当类型的新的空集合。即使目标是线程安全的,在并行情况下进行的合并也会导致错误的结果。
斯图尔特(Stuart Marks)2014年

1
@Balder我添加了一个答案,应该对此进行澄清。
斯图尔特(Stuart Marks)

4

您只需要参考原始列表即可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())
);

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


我的意思是说如何添加/收集到现有列表中,而不仅仅是重新分配。
codefx 2014年

1
嗯,从技术上讲,您不能在函数式编程范式中完成这类工作,而这正是流的全部内容。在函数式编程中,不修改状态,而是在持久性数据结构中创建新状态,从而使其可以安全地用于并发目的,并且具有更多的功能。我所提到的方法是您可以做的,或者可以诉诸于旧的面向对象方法,在该方法中,您可以遍历每个元素,并根据需要保留或删除这些元素。
阿曼·阿尼尼霍特里

0

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


0

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

我将使用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]

希望能帮助到你。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.