Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Manually chain GroupBy collectors

I want to group a list of person's. A person have some attributes like name, country, town, zipcode, etc. I wrote the static code, which works very well:

Object groupedData = data.stream().collect(groupingBy(Person::getName, Collectors.groupingBy(Person::getCountry, Collectors.groupingBy(Person::getTown))));

But the problem is, that is it not dynamic. Sometimes I want to just group by name and town, sometimes by a attributes. How can I do this? Non Java 8 solutions are welcome as well.

like image 965
Thomas Z. Avatar asked Aug 01 '16 11:08

Thomas Z.


2 Answers

You could create a function taking an arbitrary number of attributes to group by and construct the groupingBy-Collector in a loop, each time passing the previous version of itself as the downstream collector.

public static <T> Map collectMany(List<T> data, Function<T, ?>... groupers) {
    Iterator<Function<T, ?>> iter = Arrays.asList(groupers).iterator();
    Collector collector = Collectors.groupingBy(iter.next());
    while (iter.hasNext()) {
        collector = Collectors.groupingBy(iter.next(), collector);
    }
    return (Map) data.stream().collect(collector);
}

Note that the order of the grouper functions is reversed, so you have to pass them in reversed order (or reverse them inside the function, e.g. using Collections.reverse or Guava's Lists.reverse, whichever you prefer).

Object groupedData = collectMany(data, Person::getTown, Person::getCountry, Person::getName);

Or like this, using an old-school for loop to reverse the array in the function, i.e. you don't have to pass the groupers in inverse order (but IMHO this is harder to comprehend):

public static <T> Map collectMany(List<T> data, Function<T, ?>... groupers) {
    Collector collector = Collectors.groupingBy(groupers[groupers.length-1]);
    for (int i = groupers.length - 2; i >= 0; i--) {
        collector = Collectors.groupingBy(groupers[i], collector);
    }
    return (Map) data.stream().collect(collector);
}

Both approaches will return a Map<?,Map<?,Map<?,T>>>, just as in your original code. Depending on what you want to do with that data it might also be worth considering using a Map<List<?>,T>, as suggested by Tunaki.

like image 140
tobias_k Avatar answered Sep 27 '22 16:09

tobias_k


You can group by a list formed of the attributes you want to group by.

Imagine you want to group by the name and the country. Then you could use:

Map<List<Object>, List<Person>> groupedData = 
    data.stream().collect(groupingBy(p -> Arrays.asList(p.getName(), p.getCountry())));

This works because two lists are considered equal when they contain the same element in the same order. Therefore, in the resulting map, you will have a key for each different name / country pair and as value the list of persons with those specific name and country. Put another way, instead of saying "group by name, then group by country", it effectively says "group by name and country". The advantage is that you don't end-up with a map of maps of maps, etc.

like image 45
Tunaki Avatar answered Sep 27 '22 16:09

Tunaki