Fixing wild card capture error for reduce() operation with Java-Streams

Viewed 49

I want to create a generic method to calculate summation of values in the given List.

Instead of writing code below:

final BigDecimal spend = forecasts.stream()
      .filter(forecast -> Objects.nonNull(forecast.getPrice()))
      .map(PartForecastSupplierDto::getPrice)
      .reduce(BigDecimal.ZERO, BigDecimal::add);

I want to write code like this:

public <T, R> R sum(Collection<T> collection, 
                    Predicate<? super T> predicate,
                    Function<? super T, ? extends R> mapper,
                    R identity, final BinaryOperator<R> accumulator) {
    
    return CollectionUtils.emptyIfNull(collection)
        .stream()
        .filter(predicate)
        .map(mapper)
        .reduce(identity, accumulator);
}

However, I'm getting an error like this:

enter image description here

How can I fix this error?

2 Answers

When you have a chain of method invocations, the type inference will not use information of a later invocation for the preceding invocation’s target type.

Therefore, the .map(mapper) produces a Stream<? extends R> regardless of the type inferred for the subsequent reduce operation.

The simplest way to fix this, is to provide an explicit type for the map step, i.e.

public <T, R> R sum(Collection<T> collection, 
                    Predicate<? super T> predicate,
                    Function<? super T, ? extends R> mapper,
                    R identity, final BinaryOperator<R> accumulator) {
    
    return collection
        .stream()
        .filter(predicate)
        .<R>map(mapper)
        .reduce(identity, accumulator);
}

Note that I have strong objections against the policy indicated by the method name emptyIfNull. A collection should not become null and you should reject null for collections as early as possible, to eliminate the cases. Silently doing something useful with null means protracting the problem and requiring more and more of those conditionals all over the place, instead of fixing the one source of null.

Note further that your method is not summing anymore, but performing an arbitrary Reduction, so the name doesn’t fit. Also, your specific use case is testing the property it will map to, so it would be simpler the swap the order of the steps

final BigDecimal spend = forecasts.stream()
      .map(PartForecastSupplierDto::getPrice)
      .filter(Objects::nonNull)
      .reduce(BigDecimal.ZERO, BigDecimal::add);

which raises questions about the usefulness of a utility method that has a fixed constellation of filter, map, and reduce steps.

The issue arises because the type of stream elements and the type of identity doesn't match. With the version of reduce() operation you're using in your code, they should be identical:

T reduce(T identity, BinaryOperator<T> accumulator)

Where T is the type of stream element. In your case in the stream you have ? extends R, but identity is of type R.

You need another flavor of reduce() to solve the problem:

<U> U reduce(U identity,
             BiFunction<U,? super T,U> accumulator,
             BinaryOperator<U> combiner)

In such cases like accumulation BigDecimal values, you can use the same function for accumulator and combiner. And note that both functions need to be associative.

Related