Статьи

Многоуровневая группировка с потоками

1. Введение

С потоками Java 8 довольно легко группировать коллекции объектов на основе различных критериев. В этом посте мы увидим, как мы можем сделать из простых одноуровневых группировок более сложные, включающие несколько уровней группировок.

Мы будем использовать два класса для представления объектов, по которым мы хотим сгруппировать: person и pet.

Person.class

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 +
            '}';
    }
}

В основном методе мы создаем коллекцию, которую будем использовать в следующих разделах.

1
2
3
4
5
6
7
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);
  • Вы можете взглянуть на исходный код здесь .

2. Одноуровневая группировка

Самая простая форма группировки — это группировка по одному уровню. В этом примере мы собираемся сгруппировать всех людей в коллекции по их стране:

1
2
3
4
5
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"));
}

Если мы посмотрим на карту, мы увидим, как каждая страна содержит список своих граждан:

singleGroup

Результат показывает лиц, проживающих в указанной стране:

1
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. Двухуровневая группировка

В этом примере мы будем группировать не только по странам, но и по городам. Для этого нам нужно реализовать двухуровневую группировку. Мы сгруппируем людей по странам и для каждой страны, мы сгруппируем их по городам, в которых они живут.

Чтобы разрешить многоуровневую группировку, метод groupingBy в классе Collectors поддерживает дополнительный Collector в качестве второго аргумента:

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

Давайте использовать этот метод для реализации нашей двухуровневой группировки:

1
2
3
4
5
6
7
8
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());
}

Если мы отладим выполнение, мы увидим, как распределяются люди:

twoLevelGrouping

4. Трехуровневая группировка

В нашем последнем примере мы сделаем еще один шаг и сгруппируем людей по стране, городу и имени питомца. Я разделил его на два метода для удобства чтения:

01
02
03
04
05
06
07
08
09
10
11
12
13
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()));
}

Теперь у нас есть три вложенные карты, содержащие каждый список людей:

threeLevelGrouping

5. Заключение

Java 8 Collectors API предоставляет нам простой способ группировки наших коллекций. Вложив коллекторы, мы можем добавить разные слои групп для реализации многоуровневых группировок.