Collectors.converting
Peter Levart
peter.levart at gmail.com
Tue Jan 29 20:30:37 UTC 2019
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
More information about the core-libs-dev
mailing list