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<Person> 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<Integer, Data> 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<Collector<Person, ?, ?>>
Is it possible to do something like this?
In Java8 Streams, performance is achieved by parallelism, laziness, and using short-circuit operations, but there is a downside as well, and we need to be very cautious while choosing Streams, as it may degrade the performance of your application.
With Java 8, Collection interface has two methods to generate a Stream.
For this particular test, streams are about twice as slow as collections, and parallelism doesn't help (or either I'm using it the wrong way?).
The groupingBy() method of Collectors class in Java are used for grouping objects by some property and storing results in a Map instance.
For the concrete problem of summing and averaging, use collectingAndThen
along with summarizingDouble
:
Map<Integer, Data> 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<Collector<Person, ?, ?>> collectors = Arrays.asList(
Collectors.averagingInt(Person::getAge),
Collectors.summingInt(Person::getAge));
@SuppressWarnings("unchecked")
Collector<Person, List<Object>, List<Object>> complexCollector = Collector.of(
() -> collectors.stream().map(Collector::supplier)
.map(Supplier::get).collect(toList()),
(list, e) -> IntStream.range(0, collectors.size()).forEach(
i -> ((BiConsumer<Object, Person>) collectors.get(i).accumulator()).accept(list.get(i), e)),
(l1, l2) -> {
IntStream.range(0, collectors.size()).forEach(
i -> l1.set(i, ((BinaryOperator<Object>) 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<Object, Object>)collectors.get(i).finisher()).apply(list.get(i))));
return list;
});
Map<Integer, List<Object>> 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.
By using a map as an output type one could have a potentially infinite list of reducers each producing its own statistic and adding it to the map.
public static <K, V> Map<K, V> addMap(Map<K, V> map, K k, V v) {
Map<K, V> mapout = new HashMap<K, V>();
mapout.putAll(map);
mapout.put(k, v);
return mapout;
}
...
List<Person> 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));
List<BiFunction<Map<String, Integer>, Person, Map<String, Integer>>> listOfReducers = new ArrayList<>();
listOfReducers.add((m, p) -> addMap(m, "Count", Optional.ofNullable(m.get("Count")).orElse(0) + 1));
listOfReducers.add((m, p) -> addMap(m, "Sum", Optional.ofNullable(m.get("Sum")).orElse(0) + p.i1));
BiFunction<Map<String, Integer>, Person, Map<String, Integer>> applyList
= (mapin, p) -> {
Map<String, Integer> mapout = mapin;
for (BiFunction<Map<String, Integer>, Person, Map<String, Integer>> f : listOfReducers) {
mapout = f.apply(mapout, p);
}
return mapout;
};
BinaryOperator<Map<String, Integer>> combineMaps
= (map1, map2) -> {
Map<String, Integer> mapout = new HashMap<>();
mapout.putAll(map1);
mapout.putAll(map2);
return mapout;
};
Map<String, Integer> map
= persons
.stream()
.reduce(new HashMap<String, Integer>(),
applyList, combineMaps);
System.out.println("map = " + map);
Produces :
map = {Sum=10, Count=6}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With