2

I have a stream of enum values that I want to reduce. If the stream is empty or contains different values, I want null. If it only contains (multiple instances of) a single value, I want that value.

[]              null
[A, B, A]       null
[A]             A
[A, A, A]       A

I tried to do it with a reduce:

return <lots of filtering, mapping, and other stream stuff>
       .reduce((enum1, enum2) -> enum1 == enum2 ? enum1 : null)
       .orElse(null);

Unfortunately, this does not work, because this reduce method throws a NullPointerException when the result is null. Does anyone know why that happens? Why is null not a valid result?

For now, I solved this like this:

MyEnum[] array = <lots of filtering, mapping, and other stream stuff>
                 .distinct()
                 .toArray(MyEnum[]::new);
return array.length == 1 ? array[0] : null;

While this works, I am not satisfied with this "detour". I liked the reduce because it seemed to be the right fit and put everything into one stream.

Can anyone think of an alternative to the reduce (that ideally is not too much code)?

Malte Hartwig
  • 4,477
  • 2
  • 14
  • 30
  • This answer might give you some insight into why http://stackoverflow.com/a/17115490/1544715 – Magnus Mar 03 '17 at 14:45

3 Answers3

1

Generally, all Stream methods returning an Optional don’t allow null values as it would be impossible to tell the null result and “no result” (empty stream) apart.

You can work-around this with a place-holder value, which unfortunately requires to suspend the type safety (as there is no type-compatible value outside the enum set):

return <lots of filtering, mapping, and other stream stuff>
    .reduce((enum1, enum2) -> enum1 == enum2? enum1: "")
    .map(r -> r==""? null: (MyEnum)r)
    .orElse(null);

Optional.map will return an empty optional if the mapping function returns null, so after that step, an empty stream and a null result can’t be distinguished anymore and orElse(null) will return null in both cases.

But maybe the array detour only feels to unsatisfying, because the array isn’t the best choice for the intermediate result? How about

EnumSet<MyEnum> set = <lots of filtering, mapping, and other stream stuff>
    .collect(Collectors.toCollection(() -> EnumSet.noneOf(MyEnum.class)));
return set.size()==1? set.iterator().next(): null;

The EnumSet is only a bitset, a single long value if the enum type has not more than 64 constants. That’s much cheaper than an array and since Sets are naturally distinct, there is no need for a distinct() operation on the stream, which would create a HashSet under the hood.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Thanks for the explanation. I do understand the reason you give, though the reduce I tried still looked very nice :D. I have tried collecting with `toSet()` before switching to the array (thought it was more concise and possible faster). I didn't know of the EnumSet, I will give it a try. – Malte Hartwig Mar 04 '17 at 11:54
0

Most stream higher-order functions don't allow null either as parameter or function return value. They are to prevent yet another billion-dollar mistake. Such response is documented here:

Optional reduce(BinaryOperator accumulator)

.....

Throws: NullPointerException - if the result of the reduction is null

Andy Turner
  • 137,514
  • 11
  • 162
  • 243
glee8e
  • 6,180
  • 4
  • 31
  • 51
0

How about a really mathematical approach(hard to maintain I agree)?

Arrays.stream(array).map(e -> e.ordinal() + 1).reduce(Integer::sum)
            .map(i -> (double) i / array.length == array[0].ordinal() + 1 ? array[0] : null)
            .orElse(null)
Eugene
  • 117,005
  • 15
  • 201
  • 306
  • There is no array to begin with and generating an array is already part of the work-around the OP want to get rid of. – Holger Mar 03 '17 at 19:03