2

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.

Michael
  • 41,989
  • 11
  • 82
  • 128
Kartik
  • 23
  • 3
  • I guess that Java got a bit smarter at short-circuiting the count, so it skips the map as it doesn't need to evaluate it to obtain the count. That is just a guess though. – Mark Rotteveel Dec 05 '22 at 17:00
  • 2
    See also the note on [`Stream.count()`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/stream/Stream.html#count()): _"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."_. – Mark Rotteveel Dec 05 '22 at 17:03
  • When doing this kind of migration, it's much more helpful if you test in small increments, even when the intermediate versions are not LTS. You've only narrowed it down to one of 9 major versions here. If you'd tested on Java 9, then 10, then 11, you would have cut down your search space by almost 90%. At it happens, this is an issue from the jump from 8 to 9 specifically. – Michael Dec 05 '22 at 17:32
  • @Michael I don’t really see how knowing that it started failing in 9 would really help. With this info you still have to review all possible changes in 9 that might affect you, and think about diffing the Javadoc, with the same result as diffing the Java 17 Javadoc in this case. I can agree it may sometimes help answerers, but you can still do it once they request it – and it wasn’t necessary here. (anyway, this particular question is a dupe) – Didier L Dec 05 '22 at 17:59
  • @DidierL googling "java 9 stream count", the answer is contained within the first result. googling "java 17 stream count" returns nothing useful. I find it utterly baffling that you think knowing the version that introduced a breaking change is useless. And I'm not talking about helping us. I'm talking about a process of migrating that will help *them* if they do it in future, or would have helped them if they did it this time. That's why I started with "When doing this kind of migration". – Michael Dec 05 '22 at 18:02
  • @Michael well, if I search "java stream count issue", I find the correct answer as well. But if one knows the problem is with _count_, maybe they should start with reading its Javadoc? This change is not mentioned in the Java 9 release notes, so no help there. But I admit I was too radical in my comment. The thing is, testing intermediate versions from 9 to 17 is time consuming (online compilers would work here though). I don’t blame OP for not doing it but they could have spent more time searching (having an MRE was nice but playing around with variations should pinpoint to the _count_). – Didier L Dec 06 '22 at 00:03
  • 1
    What is the purpose of a construct like `streamNonnulls(Collections.singleton(message)) .filter(not(Collection::isEmpty)) .findFirst() .map(Collection::stream) .orElseGet(Stream::empty)`? The result of `Collections.singleton(message)` is never `null` and never empty, so the result of the entire expression is know at compile-time to be equivalent to `Stream.of(message)`. And using that straight-forward expression would still reproduce your problem. – Holger Dec 09 '22 at 17:28

1 Answers1

3

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.

Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
  • 1
    The documentation of `Stream.count()` also explicitly calls this out (see my comment on the question). It might be worthwhile to include it in your answer. – Mark Rotteveel Dec 05 '22 at 17:18