Hi,
I wonder if there's any interest in adding a convenience factory
method for a Collector to the standard repertoire which would look
like the following:
/**
* Convert given {@link Collector} so that it applies an
additional finisher function that
* converts the result of given collector. The characteristics
of returned collector is
* the same as that of the given inner collector but without any
* {@link
java.util.stream.Collector.Characteristics#IDENTITY_FINISH}.
*
* @param collector the inner collector to delegate
collection to
* @param resultConverter the additional result finisher function
* @param <T> the type of input stream elements
* @param <A> the type of intermediate aggregation
* @param <R> the type of result of inner collector
* @param <RR> the type of final result
* @return the converted collector
*/
public static <T, A, R, RR> Collector<T, A, RR> converting(
Collector<T, A, R> collector,
Function<? super R, RR> resultConverter
) {
return new Collector<T, A, RR>() {
@Override
public Supplier<A> supplier() { return
collector.supplier(); }
@Override
public BiConsumer<A, T> accumulator() { return
collector.accumulator(); }
@Override
public BinaryOperator<A> combiner() { return
collector.combiner(); }
@Override
public Function<A, RR> finisher() { return
resultConverter.compose(collector.finisher()); }
@Override
public Set<Characteristics> characteristics() {
EnumSet<Characteristics> characteristics =
EnumSet.noneOf(Characteristics.class);
characteristics.addAll(collector.characteristics());
characteristics.remove(Characteristics.IDENTITY_FINISH);
return Collections.unmodifiableSet(characteristics);
}
};
}
The rationale is as follows... Composability of collectors allows
doing things like:
interface Measurement {
long value();
String type();
}
Stream<Measurement> measurements = ....
Map<String, LongSummaryStatistics> statsByType =
measurements
.collect(
groupingBy(
Measurement::type,
summarizingLong(Measurement::value)
)
);
...but say I wanted the final result to be a map of avarage values
by type and the values to be BigDecimal objects calculated with
scale of 19. I can create an intermediate map like above and then
transform it to new map, like this:
static BigDecimal toBDAverage(LongSummaryStatistics stats) {
return BigDecimal.valueOf(stats.getSum())
.divide(
BigDecimal.valueOf(stats.getCount()),
20, RoundingMode.HALF_EVEN);
}
Map<String, BigDecimal> averageByType =
statsByType
.entrySet()
.stream()
.collect(toMap(Map.Entry::getKey, e ->
toBDAverage(e.getValue())));
...this is ugly as it requires intermediate result to be
materialized as a HashMap. Wouldn't it be possible to collect the
original stream to final result in one go?
With the above Collectors.converting factory method, it would:
Map<String, BigDecimal> averageByType =
measurements
.collect(
groupingBy(
Measurement::type,
converting(
summarizingLong(Measurement::value),
stats -> toBDAverage(stats)
)
)
);
We already have Collectors.mapping(Function, Collector) method that
constructs Collector that maps elements to be collected by inner
collector. Collectors.converting(Collectors, Function) would be a
twin brother that constructs Collector that converts the collection
result of the inner collector. Both are useful in compositions like
above, but we only have the 1st...
What do you think?
Regards, Peter