Core Java

Multi level grouping with streams

1. Introduction

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 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 here.

2. 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:

singleGroup

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'}]

3. 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:

twoLevelGrouping

4. Three level grouping

In our final example, we will take a step further and group people by country, city and pet name. I have splitted 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:

threeLevelGrouping

5. 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.

Reference: Multi level grouping with streams from our JCG partner Xavier Padro at the Xavier Padró’s Blog blog.

Xavier Padro

Xavier is a software developer working in a consulting firm based in Barcelona. He is specialized in web application development with experience in both frontend and backend. He is interested in everything related to Java and the Spring framework.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

6 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Thanh
Thanh
7 years ago

Excellent tutorial!

Alexander H.
Alexander H.
6 years ago

Great tutorial! Thanks a lot!

Alex
Alex
5 years ago

How to create grouping if the number of arguments can be variable?

Andrea
Andrea
5 years ago

many thanks, clear and proficient!

invite
invite
2 years ago

Where is the output ?

Rahul Kumar
Rahul Kumar
2 years ago

public static 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());
}

–> Here, groupingBy(Person::getCity) giving red line error….. Why ?

Last edited 2 years ago by Rahul Kumar
Back to top button