Custom Map Collector

3.8k Views Asked by At

I have a collection constisting of Map<Pair<DateTime, String>, List<Entity>> which was previously grouped using streams. Entity is a simple class with int property and getValue() method.

Now, I want to aggregate values of Entity with usage of my simple EntityAccumulator modyfing the type of the previous map to Map<Pair<DateTime, String>, EntityAccumulator>. The only way to achieve this as far as I understand is to create my own custom collector, howevere I've stucked at finisher() method which should return Pair.

Or, maybe is there simpler way to achieve the result I want ?

StreamProcessing

 Map<Pair<DateTime, String>, EntityAccumulator> collect = entities.stream()
                .collect(Collectors.groupingBy(entity-> Pair.of(entity.getTimestamp(), entity.getName())))
                .entrySet().stream()
                .collect(new EntityCollector()));

EntityAccumulator

private static class EntityAccumulator {

        private int result = 0.0;

        public EntityAccumulator() { }

        public EntityAccumulator(int result) {
            this.result = result;
        }

        public void calculate(Entity entity) {
            result += entity.getValue();
        }

        public EntityAccumulatoradd(EntityAccumulator other) {
            return new EntityAccumulator(this.result + other.result);
        }
}

Collector

public class EntityCollector implements Collector<Map.Entry<Pair<DateTime, String>, List<Entity>>, EntityAccumulator, Map.Entry<Pair<DateTime, String>, EntityAccumulator>> {

    @Override
    public Supplier<EntityAccumulator> supplier() {
        return EntityAccumulator::new;
    }

    @Override
    public BiConsumer<EntityAccumulator, Map.Entry<Pair<DateTime, String>, List<Entity>>> accumulator() {
        return (result, pairListEntry) -> pairListEntry.getValue().forEach(result::calculate);
    }

    @Override
    public BinaryOperator<EntityAccumulator> combiner() {
        return EntityAccumulator::add;
    }

    @Override
    public Function<EntityAccumulator, Map.Entry<Pair<DateTime, String>, EntityAccumulator>> finisher() {
        return (k) -> {
            return  null; // ??? HELP HERE 
        }
    }


    @Override
    public Set<Characteristics> characteristics() {
        return EnumSet.of(Characteristics.UNORDERED);
    }
}
1

There are 1 best solutions below

3
On BEST ANSWER

Apparently, you actually want to do

Map<Pair<DateTime, String>, Double> collect = entities.stream()
    .collect(Collectors.groupingBy(
        entity -> Pair.of(entity.getTimestamp(), entity.getName()),
        Collectors.summingDouble(Entity::getValue)));

or

Map<Pair<DateTime, String>, Integer> collect = entities.stream()
    .collect(Collectors.groupingBy(
        entity -> Pair.of(entity.getTimestamp(), entity.getName()),
        Collectors.summingInt(Entity::getValue)));

depending on the actual value type. Your declaration int result = 0.0 isn’t quite clear.

First, if you want to perform reduction on the groups, you should provide the Collector for the values as a second argument to the groupingBy collector. Then, it doesn’t have to deal with neither, Map nor Map.Entry.

Since it’s basically folding the entities to a single number (for each group), you can use an existing collector, i.e. summingInt or summingDouble.

When you create your own collector, you can’t reconstitute information in the finisher function that you have dropped in the accumulator function. If your container type EntityAccumulator contains a single number only, there is no way to produce a Map.Entry<Pair<DateTime, String>, EntityAccumulator> from it.

By the way, you rarely need to implemented the Collector interface with a class, even when creating a custom collector. You can simply use Collector.of, specifying the functions and characteristics, to create a Collector.

So using your original EntityAccumulator class (assuming, result should be int and 0.0 is a typo), you could use

Map<Pair<DateTime, String>, Integer> collect = entities.stream()
    .collect(Collectors.groupingBy(
        entity -> Pair.of(entity.getTimestamp(), entity.getName()),
        Collector.of(EntityAccumulator::new,
                     EntityAccumulator::calculate,
                     EntityAccumulator::add,
                     ea -> ea.result,
                     Collector.Characteristics.UNORDERED)));

to achieve the same as above. It would also be possible to perform the operation in two steps, like in your attempt, using

Map<Pair<DateTime, String>, Integer> collect = entities.stream()
    .collect(Collectors.groupingBy(e -> Pair.of(e.getTimestamp(), e.getName())))
    .entrySet().stream()
    .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().stream().collect(
        Collector.of(EntityAccumulator::new,
                     EntityAccumulator::calculate,
                     EntityAccumulator::add,
                     ea -> ea.result,
                     Collector.Characteristics.UNORDERED))));

but, of course, this is only for completeness. The solution shown at the beginning of this answer is simpler and more efficient.