NPE on java stream reduce operation

Viewed 9509

Recently, while working with Java 8 streams, I came across a NullPointerException on a reduce operation while working with the following test cases:

private static final BinaryOperator<Integer> sum = (a, b) -> {
    if (a == null) return b;
    if (b == null) return a;
    return Integer.sum(a, b);
};

List<Integer> s = new ArrayList<>();
s.add(null);
s.add(null);
s.add(null);

Integer i = s.stream().reduce(sum).orElse(null);
// throws NPE

Integer i = s.stream().reduce(sum).orElse(2);
// throws NPE

Integer i = s.stream().reduce(null,(a, b)->null);
// returns a value i.e null

Or alternatively:

Integer i = s.stream().filter(Objects::nonNull).reduce(Integer::sum).orElse(null);
// returns a value i.e null

Upon checking the reduce operation, I came across this class which performs the reduce operation:

class ReducingSink implements AccumulatingSink<T, Optional<T>, ReducingSink> {
    private boolean empty;
    private T state;

    public void begin(long size) {
        empty = true;
        state = null;
    }

    @Override
    public void accept(T t) {
        if (empty) {
            empty = false;
            state = t;
        } else {
            state = operator.apply(state, t);
        }
    }

    @Override
    public Optional<T> get() {
        return empty ? Optional.empty() : Optional.of(state);
    }

    @Override
    public void combine(ReducingSink other) {
        if (!other.empty)
            accept(other.state);
    }
}

In the above code, you see that the get() method returns an optional value if the boolean empty is false, and in my case the value is false but state is null, so Optional.of(null) throws a NullPointerException. In my case I have a binary operator which allows null.

So I think the code

return empty ? Optional.empty() : Optional.of(state);

should be changed to

return empty || state == null ? Optional.empty() : Optional.of(state);

As my binary operator (which has the task of reducing) and is okay with null.

3 Answers

About the reason why they decided not to deal with null values I suspect it has something to do with the fact that Optional.of takes a mandatory value and you have to use the special factory ofNullable to deal with actually optional variables... For the how to achieve the reduction I have a solution: it's a bit clunky, but you can use only built-in stream transformations.
First of all, change your accumulator to work with Optional instead of null:

private static final BinaryOperator<Optional<Integer>> sum = (a, b) -> a.isPresent()?
        (b.isPresent()? a.map(n -> b.get() + n) : a) : b;

Then wrap your elemnts in an Optional before reducing and unwrap the result after finishing:

Integer i = s.stream()
   .map(Optional::ofNullable)    // wrap nullables into Optional
   .reduce(sum)                  // get an Optional<Optional<Integer>>
   .flatMap(Function.identity()) // unwrap to an Optional<Integer>
   .orElse(null);                // if stream is empty or reduction is emptyOptional
Related