Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understand the side effect of modifying the backing collection of a stream

I wrote the following code to test the side effect of modifying the backing collection of a stream

List<Integer> x = new ArrayList<>(Arrays.asList(1, 11, 21));
x.stream().filter(i -> {
                  if (i < 10) {
                      x.remove(i); 
                      return false;
                  }
                  return true;
}).forEach(System.out::println);

The output will be

21
Exception in thread "main" java.lang.NullPointerException

Can anyone tell me what's going here? In particular, where is the NullPointerException coming from? Thank you.

like image 808
student Avatar asked Jun 20 '18 21:06

student


People also ask

Does a stream modify original list?

The stream method obtains a Stream from a java. util. Collection or a Java array. The stream operations do not modify the original collection object.

What is side effect stream Java?

A side effect is an action taken from a stream operation which changes something externally. The change may be changing some variable values in the program or it can be sending a JMS message, sending email, printing with System.

How does Stream API works internally in Java 8?

Introduced in Java 8, the Stream API is used to process collections of objects. A stream is a sequence of objects that supports various methods which can be pipelined to produce the desired result. A stream is not a data structure instead it takes input from the Collections, Arrays or I/O channels.


2 Answers

Your are violating the contract of filter by passing an interfering predicate. See here for a good explanation of the non-interference requirement. It specifically says:

The need for non-interference applies to all pipelines, not just parallel ones. Unless the stream source is concurrent, modifying a stream's data source during execution of a stream pipeline can cause exceptions, incorrect answers, or nonconformant behavior.

So the answer to your question is that the version of java you are running failed in that particular way with that particular data due to some unspecified implementation detail. A different version of java might fail in some other way or not throw anything and produce the answer you expected or produce an incorrect answer.

You can try it with a Collection that is specified to produce a CONCURRENT spliterator:

Collection<Integer> x = new ConcurrentLinkedQueue<>(Arrays.asList(1, 11, 21));

It should do what you expect and not throw any exceptions.

like image 109
Misha Avatar answered Sep 25 '22 20:09

Misha


To understand it lets make a schema about what happen exactly :

Your list look like this :

+---+       +---+       +---+      
| 1 |  -->  |11 |  -->  |21 |
+---+       +---+       +---+
  0           1           2

Now the filter is work which will iterate over your list index by index

First Iteration (index = 0, i = 1)

check if i < 10 -> yes then remove it from the list. now look like this :

+---+       +---+       +-----+      
|11 |  -->  |21 |  -->  |null |
+---+       +---+       +-----+
  0           1           2

Second Iteration (index = 1, i = 21 not 11)

check if i < 10 -> yes then remove it from the list. now look like this :

+---+       +-----+       +-----+      
|21 |  -->  |null |  -->  |null |
+---+       +-----+       +-----+
  0            1             2

Third Iteration (index = 2, i = null)

check if i < 10 which mean null < 10 -> This will throw NullPointerException.


The question now, why you do that? all you need is just :

x.removeIf(i -> i < 10);
x.forEach(System.out::println);
like image 38
YCF_L Avatar answered Sep 26 '22 20:09

YCF_L