My example is about BigDecimal but might apply to other reducable (summable) classes also.
@Test
void streamWithNullsTest() {
final var list = new ArrayList<BigDecimal>();
// Ok, gives seed null for empty list
assertNull(list.stream().reduce(null, BigDecimal::add));
// Ok, gives seed 1 for empty list
assertEquals(BigDecimal.ONE, list.stream().filter(Objects::nonNull).reduce(BigDecimal.ONE, BigDecimal::add));
// Ok, if nulls filtered, gives seed null.
list.add(null);
assertNull(list.stream().filter(Objects::nonNull).reduce(null, BigDecimal::add));
// Ok, if nulls filtered, gives seed 1.
assertEquals(BigDecimal.ONE, list.stream().filter(Objects::nonNull).reduce(BigDecimal.ONE, BigDecimal::add));
// Not ok, because seems to add BigDecimal.ONE to seed null?
// Need a null if all items are null or sum if any ot items is not null
list.add(BigDecimal.ONE);
assertNull(list.stream().filter(Objects::nonNull).reduce(null, BigDecimal::add));
}
So the last one is the problem. I guess I should implement some sort of reducer function (maybe with generics to apply other classes also) or so but is there any other way?
CodePudding user response:
You shouldn't use null
as the identity element. The reduce
method with identity can be regarded as this:
BigDecimal result = identity;
for (BigDecimal value : list) {
result = result.add(value);
}
As you see, if the identity is nul
, you will get a NullPointerException
. That's exactly what I got when I tried your code.
The identity value has an important rule though. For an operation f
, the identity value should be such that, for each value x
, f.apply(identity, x)
equals x
. If it doesn't, you can get some odd results. For instance:
BigDecimal result1 = Stream.of(BigDecimal.ONE, BigDecimal.TEN)
.reduce(BigDecimal.ONE, BigDecimal::add);
// result1 is 1 (identity) 1 (first value) 10 (second value), so 12
BigDecimal result2 = Stream.of(BigDecimal.ONE, BigDecimal.TEN)
.parallel()
.reduce(BigDecimal.ONE, BigDecimal::add);
// result2 is (1 (identity) 1 (first value)) (1 (identity) 10 (second value)), so 13
BigDecimal result3 = Stream.of(BigDecimal.ONE, BigDecimal.TEN)
.parallel()
.reduce(BigDecimal.ZERO, BigDecimal::add)
.add(BigDecimal.ONE);
// result3 is (0 (identity) 1 (first value)) (0 (identity) 10 (second value)) 1, so 12
The reason for this is that, as soon as you go parallel, the stream is split into parts, each part gets reduced on its own, and then the results of the parts are combined using the operator.
If you want to get null
as result for empty streams, use the reduce
method without identity:
list.stream()
.filter(Objects::nonNull)
.reduce(BigDecimal::add) // returns Optional<BigDecimal>
.orElse(null);