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

Collector 的 Javadoc 展示了如何将流中的元素收集到一个新的 List 中。是否有一行程序将结果添加到现有的数组列表中?

280064 次浏览

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

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

简短的回答是no(或应该是no)。编辑:是的,这是可能的(见下面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介绍/书籍的建议)

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

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

无论如何,如果您确实需要这个功能,您可以使用下面的一行程序。

但正如在其他答案中指出的,你永远都不应该这么做,特别是如果流可能是并行流-使用风险自负…

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

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

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


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

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

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

注意: nosid的答案显示如何使用__ABC0添加到现有的集合。对于改变现有集合,这是一种有用且有效的技术。我的答案是为什么你不应该使用Collector来改变一个现有的集合。

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

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

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

来自巴尔德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]

这是我上面描述的线程受限的累积/合并操作的结果。在并行流中,每个线程调用提供者来获取自己的集合以进行中间积累。如果传递一个返回相同集合的提供者,每个线程将其结果追加到该集合。由于线程之间没有顺序,结果将以任意顺序追加。

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

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

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

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

好的,那么在你使用这段代码之前,一定要记得在任何流上调用sequential():

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

当然,你每次都会记得这么做,对吧?:-)假设你有。然后,性能团队会疑惑为什么他们所有精心设计的并行实现都不能提供任何加速。他们将再次追踪到你的代码,这将迫使整个流按顺序运行。

不要这样做。

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

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

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

希望能有所帮助。

让我们说我们有一个现有的列表,并将使用java 8为这个活动 ' < / p >
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()));
}
}