Search code examples
javajava-streamjava-9

Issue with Stream processing when migrating from Java 8 to 9


The below code compiled with Java 8 works as expected but doesn't work with Java 9. Not sure what changed in the Streams execution.

import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.lang.*;

public class TestingJavaStream {
    public static void main(String[] args) {

        Message message = new Message();
        message.setName("Hello World!");

        Stream<Message> messageStream = streamNonnulls(Collections.singleton(message))
                .filter(not(Collection::isEmpty))
                .findFirst()
                .map(Collection::stream)
                .orElseGet(Stream::empty);

        System.out.println("Number of messages printed are " 
                + messageStream
                        .map(TestingJavaStream::print)
                        .count());
    }

    public static class Message {
        private String name;

        public String getName() {
            return this.name;
        }

        public void setName(String name) {
            this.name = name;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((name == null) ? 0 : name.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            Message other = (Message) obj;
            if (name == null) {
                if (other.name != null)
                    return false;
            } else if (!name.equals(other.name))
                return false;
            return true;
        }

        @Override
        public String toString() {
            return "Message [name=" + name + "]";
        }

    }

    @SafeVarargs
    public static <T> Stream<T> streamNonnulls(T... in) {
        return stream(in).filter(Objects::nonNull);
    }

    @SafeVarargs
    public static <T> Stream<T> stream(T... in) {
        return Optional.ofNullable(in)
                .filter(arr -> !(arr.length == 1 && arr[0] == null))
                .map(Stream::of)
                .orElseGet(Stream::empty);
    }

    public static <T> Predicate<T> not(Predicate<T> p) {
        return (T x) -> !p.test(x);
    }

    public static Message print(Message someValue) {
        System.out.println("Message is  :: "+someValue.toString());
        return someValue;
    }
}

The print method in the code prints the message when executed with 8 but doesn't when executed with 9.

PS: I understand the stream code can be simplified by changing the Optional logic to stream().flatmap(...) but that's beside the point.


Solution

  • You're relying on the side-effects performed via map() operation, which is discouraged by the documentation (especially if in your real code you're doing something more important than printing "hello" on the console).

    Here's a quote from the Stream API documentation:

    Side-effects in behavioral parameters to stream operations are, in general, discouraged, as they can often lead to unwitting violations of the statelessness requirement, as well as other thread-safety hazards.

    If the behavioral parameters do have side-effects, unless explicitly stated, there are no guarantees as to:

    • the visibility of those side-effects to other threads;
    • that different operations on the "same" element within the same stream pipeline are executed in the same thread; and
    • that behavioral parameters are always invoked, since a stream implementation is free to elide operations (or entire stages) from a stream pipeline if it can prove that it would not affect the result of the computation.

    The eliding of side-effects may also be surprising. With the exception of terminal operations forEach and forEachOrdered, side-effects of behavioral parameters may not always be executed when the stream implementation can optimize away the execution of behavioral parameters without affecting the result of the computation.

    Emphases added

    That means that implementations are free to elide the side-effects from the places where they are not expected or(and) would not affect the result of the stream execution.

    An example of such optimization is the behavior of the count() operation:

    An implementation may choose to not execute the stream pipeline (either sequentially or in parallel) if it is capable of computing the count directly from the stream source. In such cases no source elements will be traversed and no intermediate operations will be evaluated.".

    Since Java 9 count would optimize away operations that doesn't change the number of elements in the stream in case if the source of the stream can provide the information about the number of elements.

    Another example is eliding of the peek() operation, which according to the documentation "exists mainly to support debugging" and can be optimized away since it doesn't supposed to contribute to the result produced by the terminal operations like reduce or collect, or interfere with the resulting action performed by forEach/forEachOrdered.

    Here you can find a description of the case where peek has been elided (whilst none of the other intermediate operations was skipped).


    The bottom line: the code should not depend on the behavior, which is not guaranteed.