java - How to add elements of a Java8 stream into an existing List

ID : 20083

viewed : 22

Tags : javajava-8java-streamcollectorsjava

Top 5 Answer for java - How to add elements of a Java8 stream into an existing List

vote vote

90

NOTE: nosid's answer shows 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.

The short answer is no, at least, not in general, you shouldn't use a Collector to modify an existing collection.

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 new collection each time.

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.

A couple answers from Balder and assylias have 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.

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:

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 same collection, 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.

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 your code to break.

OK, then just make sure to remember to call sequential() on any stream before you use this code:

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 your code which is forcing the entire stream to run sequentially.

Don't do it.

vote vote

84

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 forEachOrdered in combination with a method reference.

List<String> source = ...; List<Integer> target = ...;  source.stream()       .map(String::length)       .forEachOrdered(target::add); 

The only restriction is, that source and target are 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 forEachOrdered will always be executed sequentially.

vote vote

78

The short answer is 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!

The longer answer:

The purpose of these constructs in Java 8 is to introduce some concepts of Functional Programming to 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.

If you must modify 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.

(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 bit more in the spirit of FP than 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.

The really long answer: (i.e. if you include the effort of actually finding and reading an FP intro/book as suggested)

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.

vote vote

67

Erik Allik already gave very good reasons, why you will most likely not want to collect elements of a stream into an existing List.

Anyway, you can use the following one-liner, if you really need this functionality.

But as pointed out in the other answers, you should never do this, ever, in particular not if the streams might be parallel streams - use at your own risk...

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

52

You just have to refer your original list to be the one that the Collectors.toList() returns.

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.

Top 3 video Explaining java - How to add elements of a Java8 stream into an existing List

Related QUESTION?