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:
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:
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:
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. |
Excellent tutorial!
Great tutorial! Thanks a lot!
How to create grouping if the number of arguments can be variable?
many thanks, clear and proficient!
Where is the output ?
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 ?