Are the || ('or') symbol and Stream.of(..).anyMatch(e-> e==true) functionally equivalent?

Viewed 571

Are || and Stream.of(..).anyMatch(e-> e==true) functionally equivalent when applied to the same series of conditions in the same order?

Task: Run a quick test on a series of conditions to determine whether any are true.

Possible solutions: - Separate each condition with an ‘or’ symbol (||) - Include each condition in a Stream.of(..) statement that is appended with .anyMatch(e-> e==true)

The documentation for anyMatch(..) states, “Returns whether any elements of this stream match the provided predicate. May not evaluate the predicate on all elements if not necessary for determining the result [emphasis added].”

Based on this statement, my understanding is that the two solutions indicated above are functionally the same, so that if the second element in a serious of four is the first element that is true, then elements three and four won’t be evaluated.

In practice, however, this seems not to be true. Consider the following where item is null, empty is true and UtilMethods.isEmpty(..) is a custom library method that tests if a given parameter is null or an empty String (""):

@Override
protected void updateItem(Pair<K, V> item, boolean empty) {
    super.updateItem(item, empty);

    boolean test1 = empty
             || Objects.isNull(item)
             || UtilMethods.isEmpty(item.toString())
             || Objects.isNull(item.getValue());

    boolean test2 = Stream.of(
        empty,
        isNull(item),
        UtilMethods.isEmpty(item.toString()),
        isNull(item.getValue()))
    .anyMatch(e -> e == true);
}

The || code runs as expected. However, the Stream.of(..).anyMatch(..) throws a NullPointerException when the third element is reached because item is null.

This doesn’t make sense in view of the documentation for anyMatch(..) that is referenced above, which suggests that the third element shouldn’t even be reached. Or am I missing something here?

Thanks

3 Answers

UtilMethods.isEmpty(item.toString()) is evaluated before Stream.of() is executed, so it will throw a NullPointerException regardless of whether or not you call anyMatch afterwards.

Stream.of(), just as any method call, evaluates all of its arguments before being executed, so it must evaluate UtilMethods.isEmpty(item.toString()).

You can see the same behavior in a much simpler snippet:

String s = null;
Stream<Integer> stream = Stream.of (5,s.length());

Hence, the documentation of anyMatch is irrelevant to the behavior you observed.

Your observation is correct. The two code snippets are indeed not the same. The important behaviour that || does, that Stream does not, is that || is short circuiting. When the first operand is true, || stops evaluating immediately. This is not true for the stream operation you have written. All 4 expressions:

empty,
isNull(item),
UtilMethods.isEmpty(item.toString()),
isNull(item.getValue())

Are evaluated before the stream even runs. Yes, anyMatch is lazy, but of is not. See what I mean? anyMatch will not evaluate e == true for every element if a previous element evaluated to true already. This does not apply to of.

To replicate the || behaviour, you'd have to wrap those 4 expressions into Supplier<Boolean>s, and evaluate them in anyMatch:

boolean test2 = Stream.<Supplier<Boolean>>of(
    () -> empty,
    () -> isNull(item),
    () -> UtilMethods.isEmpty(item.toString()),
    () -> isNull(item.getValue()))
    .anyMatch(Supplier::get);

As you may long know, || uses shortcut evaluation, that is, if the first item it true, the second is never evaluated. This fact saves you from a NullPointerException in hte first case.

A stream pipeline has a similar behaviour: the final anyMatch only pulls enough elements from the stream to determine whether there is a match. So it may surprise that you get the exception. The explanation of Eran is correct, though: All the arguments of Stream.of() are evaluated before of is called to create the stream. This causes the exception. In other words, the stream never gets created. So the evaluation order in the stream never gets relevant in this case.

That this must be so is probably clearer to see if for a moment we forget that this is a stream operation and just look at it as Java method calls. The structure of the calls in your stream code is similar to the following, only I have simplified it a bit:

SomeClass.foo(ref.bar()).baz();

The sunshine order of evaluation of this expression is dictated by Java:

  1. ref.bar() is evaluated to get the argument to pass to foo();
  2. foo() is called;
  3. baz() is called on the object returned from foo().

However if ref is null, the first step throws a NullPointerException and steps 2 and 3 are never performed. So there is nothing foo() nor baz() can do to change the order of evaluation nor to prevent the exception from happening. Similarly in your code there is nothing the stream pipeline can do to prevent all arguments to Stream.of() to be evaluated before of() is called to create the stream.

The rules about the order of evaluation were laid down in Java long before streams were introduced in the library and are similar to other programming languages. They wisely did not change them radically when introducing streams in Java 8. Sweeper in his answer shows a nice way to obtain the evaluation order you had expected.

Shortcut evaluation is explained in many places. One is here: What is the difference between the | and || or operators?

Related