如何将Java8stream的元素添加到现有列表中
收集器的Javadoc显示如何将stream的元素收集到新列表中。 有没有一个单线程将结果添加到现有的ArrayList中?
注意: nosid的答案显示了如何使用forEachOrdered()
添加到现有的集合。 这是改变现有集合的有用和有效的技术。 我的回答解决了为什么你不应该使用Collector
来改变现有的集合。
简短的回答是,至less,不是一般的,你不应该使用Collector
来修改现有的集合。
原因是收集器被devise为支持并行性,甚至是不是线程安全的集合。 他们这样做的方式是让每个线程独立运行,并自行收集中间结果。 每个线程获取自己的集合的方式是调用每次返回新集合所需的Collector.supplier()
。
这些中间结果的集合然后以线程限制的方式被合并,直到有一个结果集合。 这是collect()
操作的最终结果。
来自Balder和assylias的几个答案build议使用Collectors.toCollection()
,然后传递一个返回现有列表而不是新列表的供应商。 这违反了供应商的要求,即每次都返回一个新的空集合。
这将适用于简单的情况,如他们的答案中的例子所示。 但是,它会失败,特别是如果stream并行运行。 (图书馆的未来版本可能会以某种无法预料的方式发生变化,即使在连续的情况下也会导致失败。)
我们举一个简单的例子:
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
。 这是因为multithreading正在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]
这是我上面描述的线程限制累加/合并操作的结果。 通过并行stream,每个线程都会调用供应商来获取自己的集合以进行中间累加。 如果您传递了返回相同集合的供应商,则每个线程都会将其结果附加到该集合。 由于线程之间没有sorting,所以结果将以任意顺序附加。
然后,当这些中间集合合并,这基本上合并列表本身。 列表使用List.addAll()
进行合并,即如果源操作期间修改了源集合,则结果是未定义的。 在这种情况下, ArrayList.addAll()
执行数组复制操作,所以它最终会自我复制,这是我所期望的。 (请注意,其他List实现可能具有完全不同的行为。)无论如何,这解释了奇怪的结果和目标中重复的元素。
你可能会说,“我会确保按顺序运行我的stream”,然后继续写这样的代码
stream.collect(Collectors.toCollection(() -> existingList))
无论如何。 我build议不要这样做。 如果你控制stream,当然,你可以保证它不会并行运行。 我期望一种编程风格将出现在stream交付而不是集合的地方。 如果有人给你一个stream,并使用这个代码,如果stream恰好平行,它将失败。 更糟糕的是,有人可能会给你一个顺序stream,这段代码将正常工作一段时间,通过所有testing等。然后,一些任意时间后,系统中其他地方的代码可能会改变使用并行stream,这将导致您的代码打破。
OK,那么在使用这个代码之前,请确保记得在任何stream上调用sequential()
:
stream.sequential().collect(Collectors.toCollection(() -> existingList))
当然,你会记得每次都这样做,对吧? :-)假设你这样做。 然后,性能团队将想知道为什么他们精心制作的并行实现不提供任何加速。 再一次,他们会把它跟踪到你的代码,它迫使整个stream顺序运行。
不要这样做。
据我所知,到目前为止,所有其他的答案都使用收集器将元素添加到现有的stream中。 但是,有一个更短的解决scheme,它适用于顺序和并行stream。 您可以简单地将方法forEachOrdered与方法引用结合使用。
List<String> source = ...; List<Integer> target = ...; source.stream() .map(String::length) .forEachOrdered(target::add);
唯一的限制是, 源和目标是不同的列表,因为只要处理stream,就不允许更改stream的源。
请注意,此解决scheme适用于顺序和并行stream。 但是,它并没有从并发中受益。 传递给forEachOrdered的方法引用将始终按顺序执行。
简短的答案是否定的(或不应该是)。 编辑:是的,这是可能的(见下文assylias的答案),但继续阅读。 编辑2:但见Stuart标记的答案为什么你仍然不应该这样做的另一个原因!
更长的答案:
Java 8中这些构造的目的是向语言介绍函数式编程的一些概念; 在函数式编程中,通常不会修改数据结构,而是通过诸如map,filter,fold / reduce等转换来创build新的数据结构。
如果您必须修改旧列表,只需将映射的项目收集到一个新的列表中:
final List<Integer> newList = list.stream() .filter(n -> n % 2 == 0) .collect(Collectors.toList());
然后做list.addAll(newList)
– 再次:如果你真的必须。
(或者构build一个连接旧的和新的列表的新列表,并将其分配回list
variables – 这比addAll
的精神更多一点 )
至于API:即使API允许(再次看到assylias的答案),你应该尽量避免这样做,至less在一般情况下。 最好不要与范式(FP)作斗争,并试图学习而不是与之对抗(即使Java通常不是FP语言),只有在绝对需要时才采用“更脏”的策略。
真的很长的答案:(即如果你包括实际发现和阅读FP介绍/书的努力build议)
要找出为什么修改现有的列表通常是一个坏主意,并导致代码维护性较差 – 除非你修改一个局部variables,而且你的algorithm很短和/或微不足道,这超出了代码可维护性问题的范围 – find一个很好的介绍函数式编程(有数百),并开始阅读。 一个“预览”的解释是这样的:它在math上更合理,更容易理解不修改数据(在你的程序的大部分部分),并导致更高的水平和更less的技术(以及更人性化,一旦你的大脑从旧式的命令式思维转向)程序逻辑的定义。
Erik Allik已经给出了很好的理由,为什么你很可能不想收集一个stream的元素到现有的列表。
无论如何,如果你确实需要这个function,你可以使用下面的一行代码。
编辑:但正如斯图尔特·马克斯在他的回答中所解释的那样,如果这些stream可能是平行的stream,那么你不应该这样做 – 使用风险自负。
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); } }
以下是如何将新创build的元素添加到原始列表中的一行。
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()) );
这就是这个函数编程范式提供的。