Collector的Javadoc 显示如何将流中的元素收集到新List中。是否有一行代码将结果添加到现有的ArrayList中?
Collector的Javadoc 显示如何将流中的元素收集到新List中。是否有一行代码将结果添加到现有的ArrayList中?
注意:nosid的回答展示了如何使用forEachOrdered()
来向现有集合添加内容。这是一种对现有集合进行变异的有用且有效的技术。我的回答解释了为什么你不应该使用Collector
来修改现有集合。
简短的回答是不行,至少在一般情况下,你不应该使用Collector
来修改现有集合。
原因是收集器(collectors)旨在支持并行处理,即使是那些不是线程安全的集合。它们实现这一点的方式是让每个线程独立地在自己的中间结果集上操作。每个线程获得自己的集合的方法是调用Collector.supplier()
,该方法要求每次返回一个新的集合。
这些中间结果集然后以线程限制的方式合并,直到形成单个结果集。这是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]
这是我上面描述的线程限制的累加/合并操作的结果。使用并行流,每个线程调用供应商以获取其自己的集合进行中间累积。如果传递返回相同集合的供应商,则每个线程将其结果附加到该集合中。由于线程之间没有排序,因此结果将按任意顺序附加。
然后,当这些中间集合被合并时,实际上是将列表与其自身进行合并。使用List.addAll()
来合并列表,其中指定如果在操作期间修改源集合则结果未定义。在这种情况下,ArrayList.addAll()
执行一个数组复制操作,因此最终会将自己重复,这可能是人们期望的某种方式。(请注意,其他List实现可能具有完全不同的行为。)总之,这解释了目标中奇怪的结果和重复元素。
也许你会说,“我只需确保以顺序方式运行我的流”,然后编写如下代码:
stream.collect(Collectors.toCollection(() -> existingList))
不管怎样,我建议不要这样做。如果你能控制流,那么当然可以确保它不会并行运行。我预计会出现一种编程风格,其中流将被传递而不是集合。如果有人把一个流交给你,而你使用这段代码,如果该流恰巧是并行的,它将失败。更糟糕的是,有人可能将一个顺序流交给你,而这段代码在一段时间内可以正常工作、通过所有测试等。然后,系统中的其他代码可能会在任意的时间改变为使用并行流,这将导致你的代码出现问题。
好的,那么只需确保在使用此代码之前在任何流上调用sequential()
:
stream.sequential().collect(Collectors.toCollection(() -> existingList))
当然,你会记得每次都这样做,对吧? :-) 让我们假设你真的这么做了。那么,性能团队会想知道为什么他们精心制作的并行实现没有提供任何加速。再一次,他们将追溯到你的代码,这会强制整个流按顺序运行。forEachOrdered
。副作用包括将元素添加到现有集合中,无论该集合是否已经包含元素。如果您希望将流的元素放入一个 新 集合中,请使用 collect(Collectors.toList())
或 toSet()
或 toCollection()
。 - Stuart Marks就我所知,到目前为止所有其他的答案都使用了收集器来向现有流添加元素。然而,有一个更短的解决方案,它适用于顺序和并行流。你可以简单地使用方法forEachOrdered与方法引用结合使用。
List<String> source = ...;
List<Integer> target = ...;
source.stream()
.map(String::length)
.forEachOrdered(target::add);
唯一的限制是,source和target必须是不同的列表,因为在流被处理时,不允许对源进行更改。
请注意,此解决方案适用于顺序流和并行流。但是,它不会从并发性中受益。传递给forEachOrdered的方法引用将始终按顺序执行。
forEach(existing::add)
作为一种可能性。(https://dev59.com/bWEi5IYBdhLWcg3wMZ7a#21526973)。我也应该加上`forEachOrdered`...... - HolgerforEachOrdered
而不是 forEach
,有什么原因吗? - membersoundforEachOrdered
适用于顺序流和并行流,而forEach
则可能会在并行流中同时执行传递的函数对象。在这种情况下,必须正确同步函数对象,例如使用Vector<Integer>
。 - nosidtarget::add
调用之间的_happens-before_关系。无论从哪个线程调用该方法,都不存在数据竞争。我本来以为你知道这一点。 - nosidfinal List<Integer> newList = list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
然后执行list.addAll(newList)
— 如果你真的必须这样做。
(或者构建一个新的列表,将旧列表和新列表连接起来,并将其赋值回list
变量—这比使用addAll
更符合FP的精神。)
关于API:即使API允许这样做(请参见assylias的答案),通常情况下也应该尽量避免这样做。最好不要与范式(FP)对抗,而是学习它,只有在绝对必要的情况下才采用“不太正派”的策略。
详细解释:(即如果包括实际查找和阅读FP介绍/书籍所需的努力)
要找出为什么一般情况下修改现有列表是一个坏主意,并导致代码难以维护——除非你修改的是局部变量且你的算法很短和/或很简单,这超出了代码可维护性的范围——找一本好的函数式编程入门书籍(数百种选择)并开始阅读。一个“预览”说明是:大多数情况下,不修改数据(在程序的大多数部分)更具数学上的合理性,更容易推理,并导致更高级别、更少技术性(以及一旦你的大脑过渡到旧式命令式思维之外更加人性化)的程序逻辑定义。
Erik Kaplun已经给出了非常好的理由,解释为什么你最好不要将流中的元素收集到现有的List中。
无论如何,如果你确实需要这种功能,你可以使用下面的一行代码。
但是正如其他答案中指出的那样,你永远不应该这样做,绝不会这样做,特别是如果流可能是并行流——使用时需自负...
list.stream().collect(Collectors.toCollection(() -> myExistingList));
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())
);
我会将旧列表和新列表作为流连接起来,并将结果保存到目标列表中。并行处理也很有效。
我将使用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]
希望能有所帮助。
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()));
}
}
`
targetList = sourceList.stream().flatmap(List::stream).collect(Collectors.toList());
targetList = sourceList.stream().flatMap(List::stream).collect(Collectors.toList());