I want to use a Java 8 Stream and Group by one classifier but have multiple Collector functions. So when grouping, for example the average and the sum of one field (or maybe another field) is calculated.
I try to simplify this a bit with an example:
public void test() {
List persons = new ArrayList<>();
persons.add(new Person("Person One", 1, 18));
persons.add(new Person("Person Two", 1, 20));
persons.add(new Person("Person Three", 1, 30));
persons.add(new Person("Person Four", 2, 30));
persons.add(new Person("Person Five", 2, 29));
persons.add(new Person("Person Six", 3, 18));
Map result = persons.stream().collect(
groupingBy(person -> person.group, multiCollector)
);
}
class Person {
String name;
int group;
int age;
// Contructor, getter and setter
}
class Data {
long average;
long sum;
public Data(long average, long sum) {
this.average = average;
this.sum = sum;
}
// Getter and setter
}
The result should be a Map that associates the result of grouping like
1 => Data(average(18, 20, 30), sum(18, 20, 30))
2 => Data(average(30, 29), sum(30, 29))
3 => ....
This works perfectly fine with one function like "Collectors.counting()" but I like to chain more than one (ideally infinite from a List).
List>
Is it possible to do something like this?
解决方案
For the concrete problem of summing and averaging, use collectingAndThen along with summarizingDouble:
Map result = persons.stream().collect(
groupingBy(Person::getGroup,
collectingAndThen(summarizingDouble(Person::getAge),
dss -> new Data((long)dss.getAverage(), (long)dss.getSum()))));
For the more generic problem (collect various things about your Persons), you can create a complex collector like this:
// Individual collectors are defined here
List> collectors = Arrays.asList(
Collectors.averagingInt(Person::getAge),
Collectors.summingInt(Person::getAge));
@SuppressWarnings("unchecked")
Collector, List> complexCollector = Collector.of(
() -> collectors.stream().map(Collector::supplier)
.map(Supplier::get).collect(toList()),
(list, e) -> IntStream.range(0, collectors.size()).forEach(
i -> ((BiConsumer) collectors.get(i).accumulator()).accept(list.get(i), e)),
(l1, l2) -> {
IntStream.range(0, collectors.size()).forEach(
i -> l1.set(i, ((BinaryOperator) collectors.get(i).combiner()).apply(l1.get(i), l2.get(i))));
return l1;
},
list -> {
IntStream.range(0, collectors.size()).forEach(
i -> list.set(i, ((Function)collectors.get(i).finisher()).apply(list.get(i))));
return list;
});
Map> result = persons.stream().collect(
groupingBy(Person::getGroup, complexCollector));
Map values are lists where first element is the result of applying the first collector and so on. You can add a custom finisher step using Collectors.collectingAndThen(complexCollector, list -> ...) to convert this list to something more appropriate.