Since the actually returned type is relevant to the caller and has not much use for the caller when it still is non-obvious due to a declared type of Number, it should be under the caller’s control and combined with a generic type signature, which allows the caller to actually use a particular return type. E.g.
public static <N extends Number, R extends Number> R sum(
List<? extends N> input, Function<? super N, ? extends R> cast,
BinaryOperator<R> addition) {
return input.stream().<R>map(cast).reduce(addition).orElse(null);
}
public static <N extends Number> N sum(
List<? extends N> input, BinaryOperator<N> addition) {
return sum(input, Function.identity(), addition);
}
This allows to request the calculation to be within the input types, e.g.
List<Integer> list = Arrays.asList(1, 2, 3, 4);
Integer iSum1 = sum(list, Integer::sum);
Integer iSum2 = sum(list, Math::addExact);//throw on overflow
but also widening the type before summing up:
Long lSum = sum(list, Integer::longValue, Long::sum);
Likewise, you can handle Long or Double input types:
List<Long> list = Arrays.asList(1L, 2L, 3L, 4L);
Long lSum1 = sum(list, Long::sum);
Long lSum2 = sum(list, Math::addExact);//throw on overflow
// without precision loss:
BigInteger biSum = sum(list, BigInteger::valueOf, BigInteger::add);
List<Double> list = Arrays.asList(1.0, 2.0, 3.0, 4.0);
Double dSum = sum(list, Double::sum);
// without precision loss:
BigDecimal bdSum = sum(list, BigDecimal::valueOf, BigDecimal::add);
Or deal with mixed types:
List<Number> list = Arrays.asList(1, 2L, 3.0, 4F);
Double dSum = sum(list, Number::doubleValue, Double::sum);
BigDecimal bdSum = sum(list, n -> new BigDecimal(n.toString()), BigDecimal::add);
Note that Java’s Number type hierarchy does not reflect the type conversion rules of the primitive types. So while a mixture of int and long values could be handled as long whereas mixing int and double would require using double to prevent loss of precision, there is no difference between mixing Integer and Long vs. mixing Integer and Double, both are just mixtures of different Number subtypes. So in either case, you need a Number::xxxValue conversion in-between and regardless of the actual combination, any Number::xxxValue conversion would compile without a warning, even when it implies loss of precision.
Since large long values could lose precision when being converted to double, the last example use an intermediate String value, to ensure that, in the presence of long and double input value, all conversions to BigDecimal are lossless.