如何将 Java8 流的元素添加到现有列表中
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/22753755/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to add elements of a Java8 stream into an existing List
提问by codefx
Javadoc of Collectorshows how to collect elements of a stream into a new List. Is there an one-liner that adds the results into an existing ArrayList?
Collector 的 Javadoc展示了如何将流的元素收集到新列表中。是否有一个单行将结果添加到现有的 ArrayList 中?
采纳答案by Stuart Marks
NOTE:nosid's answershows how to add to an existing collection using forEachOrdered()
. This is a useful and effective technique for mutating existing collections. My answer addresses why you shouldn't use a Collector
to mutate an existing collection.
注意:nosid 的回答显示了如何使用forEachOrdered()
. 这是一种用于改变现有集合的有用且有效的技术。我的回答说明了为什么不应该使用 aCollector
来改变现有集合。
The short answer is no, at least, not in general, you shouldn't use a Collector
to modify an existing collection.
简短的回答是否定的,至少不是一般情况下,您不应该使用 aCollector
来修改现有集合。
The reason is that collectors are designed to support parallelism, even over collections that aren't thread-safe. The way they do this is to have each thread operate independently on its own collection of intermediate results. The way each thread gets its own collection is to call the Collector.supplier()
which is required to return a newcollection each time.
原因是收集器旨在支持并行性,即使是在非线程安全的集合上也是如此。他们这样做的方式是让每个线程独立运行在它自己的中间结果集合上。每个线程获取自己的集合的方式是调用每次Collector.supplier()
返回新集合所需的。
These collections of intermediate results are then merged, again in a thread-confined fashion, until there is a single result collection. This is the final result of the collect()
operation.
然后,这些中间结果的集合再次以线程受限的方式合并,直到有一个单一的结果集合。这是collect()
手术的最终结果。
A couple answers from Balderand assyliashave suggested using Collectors.toCollection()
and then passing a supplier that returns an existing list instead of a new list. This violates the requirement on the supplier, which is that it return a new, empty collection each time.
Balder和assylias 的一些答案建议使用Collectors.toCollection()
然后传递返回现有列表而不是新列表的供应商。这违反了对供应商的要求,即它每次都返回一个新的空集合。
This will work for simple cases, as the examples in their answers demonstrate. However, it will fail, particularly if the stream is run in parallel. (A future version of the library might change in some unforeseen way that will cause it to fail, even in the sequential case.)
这将适用于简单的情况,如他们的答案中的示例所示。但是,它会失败,特别是如果流并行运行。(库的未来版本可能会以某种不可预见的方式发生变化,这将导致它失败,即使在顺序情况下也是如此。)
Let's take a simple example:
我们举一个简单的例子:
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);
When I run this program, I often get an ArrayIndexOutOfBoundsException
. This is because multiple threads are operating on ArrayList
, a thread-unsafe data structure. OK, let's make it synchronized:
当我运行这个程序时,我经常得到一个ArrayIndexOutOfBoundsException
. 这是因为多个线程正在对ArrayList
线程不安全的数据结构 进行操作。好的,让我们让它同步:
List<String> destList =
Collections.synchronizedList(new ArrayList<>(Arrays.asList("foo")));
This will no longer fail with an exception. But instead of the expected result:
这将不再因异常而失败。但不是预期的结果:
[foo, 0, 1, 2, 3]
it gives weird results like this:
它给出了这样的奇怪结果:
[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]
This is the result of the thread-confined accumulation/merging operations I described above. With a parallel stream, each thread calls the supplier to get its own collection for intermediate accumulation. If you pass a supplier that returns the samecollection, each thread appends its results to that collection. Since there is no ordering among the threads, results will be appended in some arbitrary order.
这是我上面描述的线程限制累积/合并操作的结果。使用并行流,每个线程调用供应商以获取自己的集合以进行中间累积。如果您传递返回相同集合的供应商,则每个线程将其结果附加到该集合。由于线程之间没有排序,结果将以某种任意顺序附加。
Then, when these intermediate collections are merged, this basically merges the list with itself. Lists are merged using List.addAll()
, which says that the results are undefined if the source collection is modified during the operation. In this case, ArrayList.addAll()
does an array-copy operation, so it ends up duplicating itself, which is sort-of what one would expect, I guess. (Note that other List implementations might have completely different behavior.) Anyway, this explains the weird results and duplicated elements in the destination.
然后,当这些中间集合合并时,这基本上将列表与其自身合并。列表使用 合并List.addAll()
,这表示如果在操作期间修改了源集合,则结果是未定义的。在这种情况下,ArrayList.addAll()
执行数组复制操作,因此它最终会自我复制,我想这是人们所期望的。(请注意,其他 List 实现可能具有完全不同的行为。)无论如何,这解释了目的地中的奇怪结果和重复元素。
You might say, "I'll just make sure to run my stream sequentially" and go ahead and write code like this
你可能会说,“我会确保按顺序运行我的流”然后继续编写这样的代码
stream.collect(Collectors.toCollection(() -> existingList))
anyway. I'd recommend against doing this. If you control the stream, sure, you can guarantee that it won't run in parallel. I expect that a style of programming will emerge where streams get handed around instead of collections. If somebody hands you a stream and you use this code, it'll fail if the stream happens to be parallel. Worse, somebody might hand you a sequential stream and this code will work fine for a while, pass all tests, etc. Then, some arbitrary amount of time later, code elsewhere in the system might change to use parallel streams which will cause yourcode to break.
反正。我建议不要这样做。如果您控制流,当然可以保证它不会并行运行。我预计会出现一种编程风格,其中流被传递而不是集合。如果有人给你一个流并且你使用了这个代码,如果流恰好是并行的,它就会失败。更糟糕的是,有人可能会给你一个顺序流,这段代码可以正常工作一段时间,通过所有测试等。然后,在任意一段时间之后,系统中其他地方的代码可能会更改为使用并行流,这将导致你的代码打破。
OK, then just make sure to remember to call sequential()
on any stream before you use this code:
好的,那么请务必记住sequential()
在使用此代码之前调用任何流:
stream.sequential().collect(Collectors.toCollection(() -> existingList))
Of course, you'll remember to do this every time, right? :-) Let's say you do. Then, the performance team will be wondering why all their carefully crafted parallel implementations aren't providing any speedup. And once again they'll trace it down to yourcode which is forcing the entire stream to run sequentially.
当然,你每次都会记得这样做,对吧?:-) 假设你这样做。然后,性能团队会想知道为什么他们精心设计的并行实现没有提供任何加速。他们将再次将其追溯到您的代码,该代码迫使整个流按顺序运行。
Don't do it.
不要这样做。
回答by Aman Agnihotri
You just have to refer your original list to be the one that the Collectors.toList()
returns.
您只需将原始列表引用为Collectors.toList()
返回的列表。
Here's a demo:
这是一个演示:
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);
}
}
And here's how you can add the newly created elements to your original list in just one line.
以下是如何在一行中将新创建的元素添加到原始列表中的方法。
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())
);
That's what this Functional Programming Paradigm provides.
这就是这个函数式编程范式所提供的。
回答by Erik Kaplun
The short answeris no (or should be no). EDIT:yeah, it's possible (see assylias' answer below), but keep reading. EDIT2:but see Stuart Marks' answer for yet another reason why you still shouldn't do it!
简短的回答是否定的(或应该不是)。编辑:是的,这是可能的(请参阅下面的 assylias 回答),但请继续阅读。EDIT2:但请参阅 Stuart Marks 的回答,这是您仍然不应该这样做的另一个原因!
The longer answer:
更长的答案:
The purpose of these constructs in Java 8 is to introduce some concepts of Functional Programmingto the language; in Functional Programming, data structures are not typically modified, instead, new ones are created out of old ones by means of transformations such as map, filter, fold/reduce and many others.
Java 8 中这些构造的目的是将函数式编程的一些概念引入该语言;在函数式编程中,数据结构通常不会被修改,而是通过映射、过滤、折叠/减少等许多其他转换从旧结构中创建新结构。
If you mustmodify the old list, simply collect the mapped items into a fresh list:
如果您必须修改旧列表,只需将映射项收集到新列表中:
final List<Integer> newList = list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
and then do list.addAll(newList)
— again: if you really must.
然后再做list.addAll(newList)
——如果你真的必须这样做。
(or construct a new list concatenating the old one and the new one, and assign it back to the list
variable—this is a little bitmore in the spirit of FP than addAll
)
(或构建一个新的列表拼接旧与新的一个,并将其分配回list
变,这是一个有点更FP比灵addAll
)
As to the API: even though the API allows it (again, see assylias' answer) you should try to avoid doing that regardless, at least in general. It's best not to fight the paradigm (FP) and try to learn it rather than fight it (even though Java generally isn't a FP language), and only resort to "dirtier" tactics if absolutely needed.
至于 API:即使 API 允许(再次参见 assylias 的回答),您也应该尽量避免这样做,至少在一般情况下是这样。最好不要对抗范式 (FP) 并尝试学习它而不是对抗它(即使 Java 通常不是 FP 语言),并且只有在绝对需要时才采用“更肮脏”的策略。
The really long answer:(i.e. if you include the effort of actually finding and reading an FP intro/book as suggested)
真的很长的答案:(即,如果您按照建议包括实际查找和阅读 FP 介绍/书籍的努力)
To find out why modifying existing lists is in general a bad idea and leads to less maintainable code—unless you're modifying a local variable and your algorithm is short and/or trivial, which is out of the scope of the question of code maintainability—find a good introduction to Functional Programming (there are hundreds) and start reading. A "preview" explanation would be something like: it's more mathematically sound and easier to reason about to not modify data (in most parts of your program) and leads to higher level and less technical (as well as more human friendly, once your brain transitions away from the old-style imperative thinking) definitions of program logic.
找出为什么修改现有列表通常是一个坏主意并导致代码的可维护性降低 - 除非您正在修改局部变量并且您的算法很短和/或微不足道,这超出了代码可维护性问题的范围- 找到一个很好的函数式编程介绍(有数百个)并开始阅读。“预览”解释类似于:在数学上更合理,更容易推理不修改数据(在程序的大多数部分),并导致更高的水平和更少的技术(以及更人性化,一旦你的大脑远离旧式命令式思维)程序逻辑的定义。
回答by Balder
Erik Allikalready gave very good reasons, why you will most likely not want to collect elements of a stream into an existing List.
Erik Allik已经给出了很好的理由,为什么您很可能不想将流的元素收集到现有列表中。
Anyway, you can use the following one-liner, if you really need this functionality.
无论如何,如果您确实需要此功能,则可以使用以下单行。
But as Stuart Marksexplains in his answer, you should neverdo this, if the streams might be parallel streams - use at your own risk...
但是正如Stuart Marks在他的回答中所解释的那样,如果流可能是并行流,则永远不应该这样做 - 使用风险自负......
list.stream().collect(Collectors.toCollection(() -> myExistingList));
回答by nosid
As far as I can see, all other answers so far used a collector to add elements to an existing stream. However, there's a shorter solution, and it works for both sequential and parallel streams. You can simply use the method forEachOrderedin combination with a method reference.
据我所知,到目前为止,所有其他答案都使用收集器将元素添加到现有流中。但是,有一个更短的解决方案,它适用于顺序和并行流。您可以简单地将方法forEachOrdered与方法引用结合使用。
List<String> source = ...;
List<Integer> target = ...;
source.stream()
.map(String::length)
.forEachOrdered(target::add);
The only restriction is, that sourceand targetare different lists, because you are not allowed to make changes to the source of a stream as long as it is processed.
唯一的限制是,源和目标是不同的列表,因为只要流的源被处理,就不允许对其进行更改。
Note that this solution works for both sequential and parallel streams. However, it does not benefit from concurrency. The method reference passed to forEachOrderedwill always be executed sequentially.
请注意,此解决方案适用于顺序流和并行流。但是,它并没有从并发中受益。传递给forEachOrdered的方法引用将始终按顺序执行。
回答by A. S. Ranjan
targetList = sourceList.stream().flatmap(List::stream).collect(Collectors.toList());
targetList = sourceList.stream().flatmap(List::stream).collect(Collectors.toList());
回答by Nikos Stais
I would concatenate the old list and new list as streams and save the results to destination list. Works well in parallel, too.
我会将旧列表和新列表连接为流并将结果保存到目标列表。并行运行也很好。
I will use the example of accepted answer given by Stuart Marks:
我将使用 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]
Hope it helps.
希望能帮助到你。