Javadoc of Collector显示了如何将流的元素收集到新的List中。是否存在将结果添加到现有ArrayList的单行程序?
答案 0 :(得分:147)
注意: nosid's answer显示如何使用forEachOrdered()
添加到现有馆藏。这是一种用于改变现有集合的有用且有效的技术。我的回答解决了为什么你不应该使用Collector
来改变现有的集合。
简短的回答是否,至少在一般情况下,您不应该使用Collector
来修改现有的收藏。
原因是收集器旨在支持并行性,即使是不是线程安全的集合也是如此。他们这样做的方法是让每个线程独立地在其自己的中间结果集合上运行。每个线程获得自己的集合的方式是调用每次都返回 new 集合所需的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实现可能具有完全不同的行为。)无论如何,这解释了目标中的奇怪结果和重复元素。
你可能会说,&#34;我只是确保按顺序运行我的流&#34;然后继续写这样的代码
stream.collect(Collectors.toCollection(() -> existingList))
反正。我建议不要这样做。如果您控制流,当然,您可以保证它不会并行运行。我希望在流式传输而不是集合的情况下会出现一种编程风格。如果有人向您提供流并且您使用此代码,那么如果流恰好是并行的,它将会失败。更糟糕的是,有人可能会给你一个顺序流,这段代码可以正常工作一段时间,通过所有测试等等。然后,一些任意的时间后,系统中其他地方的代码可能会改变为使用并行流将导致你的代码要破解。
好的,然后确保在使用此代码之前记得在任何流上调用sequential()
:
stream.sequential().collect(Collectors.toCollection(() -> existingList))
当然,你记得每次都这样做,对吧? :-)让我们说你这样做。然后,性能团队将会想知道为什么他们所有精心设计的并行实现都没有提供任何加速。再一次,他们会将其追溯到您的代码,这会强制整个流按顺序运行。
不要这样做。
答案 1 :(得分:142)
据我所知,到目前为止,所有其他答案都使用了一个收集器来向现有流添加元素。但是,有一个更短的解决方案,它适用于顺序和并行流。您只需将 forEachOrdered 方法与方法参考结合使用即可。
List<String> source = ...;
List<Integer> target = ...;
source.stream()
.map(String::length)
.forEachOrdered(target::add);
唯一的限制是, source 和 target 是不同的列表,因为只要处理了流,就不允许对流的源进行更改
请注意,此解决方案适用于顺序和并行流。但是,它并没有从并发中受益。传递给 forEachOrdered 的方法引用将始终按顺序执行。
答案 2 :(得分:11)
简短回答不是(或应该是否定)。 编辑:是的,这是可能的(见下面的assylias'答案),但继续阅读。 EDIT2:,但请看Stuart Marks的答案还有另外一个原因,你仍然不应该这样做!
答案越长:
Java 8中这些结构的目的是向该语言引入Functional Programming的一些概念;在函数式编程中,通常不会修改数据结构,而是通过map,filter,fold / reduce等变换创建新的数据结构。
如果您必须修改旧列表,只需将映射的项目收集到一个新的列表中:
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简介/书籍的努力)
要找出为什么修改现有列表通常是一个坏主意并导致代码维护较少 - 除非您修改局部变量并且算法很短和/或微不足道,这超出了问题的范围代码可维护性 - 找到功能编程的好介绍(有数百个)并开始阅读。一个“预览”的解释是这样的:它在数学上更合理,更容易推理不修改数据(在程序的大多数部分)并导致更高级别和更少技术(以及更人性化,一旦你的大脑从旧式命令式思维过渡到程序逻辑的定义。
答案 3 :(得分:9)
Erik Allik已经提供了很好的理由,为什么您很可能不希望将流的元素收集到现有列表中。
无论如何,如果你真的需要这个功能,你可以使用以下单行。
但正如Stuart Marks在他的回答中所解释的那样,如果流可能是并行流,你应该从不这样做 - 使用风险自负......
list.stream().collect(Collectors.toCollection(() -> myExistingList));
答案 4 :(得分: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())
);
这就是本函数式编程范例所提供的内容。
答案 5 :(得分:0)
targetList = sourceList.stream()。flatmap(List :: stream).collect(Collectors.toList());
答案 6 :(得分: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]
希望有帮助。
答案 7 :(得分:0)
假设我们有现有列表,并且将使用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()));
}
}
`