Multi level grouping with streams

With Java 8 streams it is pretty easy to group collections of objects based on different criteria. In this post, we will see how we can make stream grouping, from simple single level groupings to more complex, involving several levels of groupings.

We will use two classes to represent the objects we want to group by: person and pet.

Person.class

public class Person {
    private final String name;
    private final String country;
    private final String city;
    private final Pet pet;
    
    public Person(String name, String country, String city, Pet pet) {
        this.name = name;
        this.country = country;
        this.city = city;
        this.pet = pet;
    }
    
    public String getName() {
        return name;
    }
    
    public String getCountry() {
        return country;
    }
    
    public String getCity() {
        return city;
    }
    
    public Pet getPet() {
        return pet;
    }
    
    @Override
    public String toString() {
        return "Person{" +
            "name='" + name + '\'' +
            ", country='" + country + '\'' +
            ", city='" + city + '\'' +
            '}';
    }
}

 

Pet.class

public class Pet {
    private final String name;
    private final int age;
    
    public Pet(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
    
    @Override
    public String toString() {
        return "Pet{" +
            "name='" + name + '\'' +
            ", age=" + age +
            '}';
    }
}

 

In the main method we create the collection we will use in the following sections.

public static void main(String[] args) {
    Person person1 = new Person("John", "USA", "NYC", new Pet("Max", 5));
    Person person2 = new Person("Steve", "UK", "London", new Pet("Lucy", 8));
    Person person3 = new Person("Anna", "USA", "NYC", new Pet("Buddy", 12));
    Person person4 = new Person("Mike", "USA", "Chicago", new Pet("Duke", 10));
    
    List<Person> persons = Arrays.asList(person1, person2, person3, person4);

 

You can take a look at the source code at my Github repository.

1 Single level grouping

The simplest form of grouping is the single level grouping. In this example we are going to group all persons in the collection by their country:

public void singleLevelGrouping(List<Person> persons) {
    final Map<String, List<Person>> personsByCountry = persons.stream().collect(groupingBy(Person::getCountry));
    
    System.out.println("Persons in USA: " + personsByCountry.get("USA"));
}

 

If we take a look into the map, we can see how each country contains a list of its citizens:

stream grouping - single level

 

The result shows persons living in the specified country:

Persons in USA: [Person{name=’John’, country=’USA’, city=’New York’}, Person{name=’Anna’, country=’USA’, city=’New York’}, Person{name=’Mike’, country=’USA’, city=’Chicago’}]

2 Two level grouping

In this example, we will group not only by country but also by city. To accomplish this, we need to implement a two level grouping. We will group persons by country and for each country, we will group its persons by the city where they live.

In order to allow multi level grouping, the groupingBy method in class Collectors supports an additional Collector as a second argument:

public static <T, K, A, D>
    Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
                                          Collector<? super T, A, D> downstream)

Let’s use this method to implement our two level grouping:

public void twoLevelGrouping(List<Person> persons) {
     final Map<String, Map<String, List<Person>>> personsByCountryAndCity = persons.stream().collect(
         groupingBy(Person::getCountry,
            groupingBy(Person::getCity)
        )
    );
    System.out.println("Persons living in London: " + personsByCountryAndCity.get("UK").get("London").size());
}

 

If we debug the execution, we will see how people is distributed:

stream grouping - two level

3 Three level grouping

In our final example, we will take a step further and group people by country, city and pet name. I have split it into two methods for readability:

public void threeLevelGrouping(List<Person> persons) {
    final Map<String, Map<String, Map<String, List<Person>>>> personsByCountryCityAndPetName = persons.stream().collect(
            groupingBy(Person::getCountry,
                groupByCityAndPetName()
            )
    );
    System.out.println("Persons whose pet is named 'Max' and live in NY: " +
        personsByCountryCityAndPetName.get("USA").get("NYC").get("Max").size());
}

private Collector<Person, ?, Map<String, Map<String, List<Person>>>> groupByCityAndPetName() {
    return groupingBy(Person::getCity, groupingBy(p -> p.getPet().getName()));
}

 

Now we have three nested maps containing each list of persons:

stream grouping - three level

 

4 Conclusion

The Java 8 Collectors API provides us with an easy way to group our collections. By nesting collectors, we can add different layers of groups to implement multi level groupings.

I’m publishing my new posts on Google plus and Twitter. Follow me if you want to be updated with new content.