JavaRush /Курсы /JAVA 25 SELF /Методы groupingBy и partitioningBy (Collectors)

Методы groupingBy и partitioningBy (Collectors)

JAVA 25 SELF
31 уровень , 2 лекция
Открыта

1. Введение

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

Без Stream API такие задачи решались вручную: перебор коллекции, проверка признака, добавление в нужный список в Map. Вот типичный «олдскульный» код:

Map<String, List<Employee>> byDepartment = new HashMap<>();
for (Employee employee : employees) {
    String dept = employee.getDepartment();
    byDepartment.computeIfAbsent(dept, k -> new ArrayList<>()).add(employee);
}

Работает, но выглядит как будто вы вручную сортируете макароны по банкам. А если нужно сгруппировать по нескольким признакам? Или ещё и посчитать суммы по группам? Код разрастается и становится нечитаемым.

Stream API и специальные коллекторы (groupingBy, partitioningBy) позволяют делать это в одну-две строки — и выглядеть при этом как настоящий Java-волшебник.

2. Коллектор groupingBy: группировка по признаку

Основная идея

groupingBy — это коллектор, который превращает поток элементов в Map, где ключ — результат функции-признака, а значение — список элементов, соответствующих этому признаку.

Сигнатура:

Collectors.groupingBy(Function<T, K>)
  • T — тип элемента в потоке,
  • K — тип ключа (группы), который возвращает функция.

Простой пример: группировка сотрудников по отделу

Допустим, у нас есть класс:

public class Employee {
    private final String name;
    private final String department;
    // ... конструктор и геттеры
    public Employee(String name, String department) {
        this.name = name;
        this.department = department;
    }
    public String getName() { return name; }
    public String getDepartment() { return department; }
}

И коллекция сотрудников:

List<Employee> employees = List.of(
    new Employee("Алиса", "IT"),
    new Employee("Боб", "HR"),
    new Employee("Клара", "IT"),
    new Employee("Денис", "Finance"),
    new Employee("Ева", "HR")
);

Группируем по отделу:

Map<String, List<Employee>> byDepartment = employees.stream()
    .collect(Collectors.groupingBy(Employee::getDepartment));

Что получили?

  • Ключ: название отдела (String).
  • Значение: список сотрудников этого отдела (List<Employee>).

Печатаем результат:

byDepartment.forEach((dept, emps) -> {
    System.out.println(dept + ": " +
        emps.stream().map(Employee::getName).toList());
});

Вывод:

IT: [Алиса, Клара]
HR: [Боб, Ева]
Finance: [Денис]

Как это работает под капотом?

Сначала для каждого элемента потока вычисляется ключ (например, getDepartment()). Если такой ключ уже есть в Map, элемент добавляется в соответствующий список. Если ключа нет — создаётся новый список.

Аналогия

Представьте, что вы сортируете письма по папкам: для каждого письма смотрите, есть ли уже папка с нужным названием, если нет — заводите новую и кладёте письмо туда.

3. Коллектор partitioningBy: разделение на две группы

Иногда нужно не столько «группировать по значению», сколько просто разбить коллекцию на две части по логическому признаку (true/false). Например, сотрудников с зарплатой выше и ниже определённого порога, студентов — на «сдал/не сдал».

Для этого есть особый коллектор — partitioningBy.

Сигнатура:

Collectors.partitioningBy(Predicate<T>)

Predicate — функция, возвращающая булево значение.

Пример: разделение сотрудников по уровню зарплаты

Допустим, у нас есть:

public class Employee {
    private final String name;
    private final int salary;
    // ... конструктор и геттеры
    public Employee(String name, int salary) {
        this.name = name;
        this.salary = salary;
    }
    public String getName() { return name; }
    public int getSalary() { return salary; }
}

И список:

List<Employee> employees = List.of(
    new Employee("Алиса", 120_000),
    new Employee("Боб", 80_000),
    new Employee("Клара", 150_000),
    new Employee("Денис", 95_000)
);

Разделим на «богатых» и «скромных»:

Map<Boolean, List<Employee>> partitioned = employees.stream()
    .collect(Collectors.partitioningBy(e -> e.getSalary() > 100_000));
  • true — сотрудники с зарплатой больше 100_000.
  • false — остальные.

Печатаем:

System.out.println("Богатые: " +
    partitioned.get(true).stream().map(Employee::getName).toList());
System.out.println("Скромные: " +
    partitioned.get(false).stream().map(Employee::getName).toList());

Результат:

Богатые: [Алиса, Клара]
Скромные: [Боб, Денис]

Когда использовать partitioningBy, а когда groupingBy?

  • Если групп больше двух — используйте groupingBy.
  • Если только две группы по булевому признаку — используйте partitioningBy: это быстрее и понятнее.

4. Вложенные группировки: группировка по нескольким признакам

Иногда хочется сгруппировать не только по одному признаку, но и «вложить» одну группировку в другую. Например, сгруппировать сотрудников сначала по отделу, а внутри отдела — по должности.

Пример:

Допустим, в нашем классе Employee есть ещё поле position:

public class Employee {
    private final String name;
    private final String department;
    private final String position;
    // ... конструктор и геттеры
}

Вложенная группировка:

Map<String, Map<String, List<Employee>>> byDeptAndPosition = employees.stream()
    .collect(Collectors.groupingBy(Employee::getDepartment,
        Collectors.groupingBy(Employee::getPosition)));
  • Внешний ключ — отдел.
  • Внутренний ключ — должность.
  • Значение — список сотрудников.

Как обратиться к сотрудникам из IT-отдела, занимающим должность "Developer"?

List<Employee> itDevs = byDeptAndPosition
    .getOrDefault("IT", Map.of())
    .getOrDefault("Developer", List.of());

Визуальная схема

Map<Department, Map<Position, List<Employee>>>
└─ "IT"
    ├─ "Developer" -> [Алиса, Клара]
    └─ "QA"        -> [Борис]
└─ "HR"
    └─ "Recruiter" -> [Денис]

5. Практические примеры группировки

Пример 1: Группировка строк по длине

List<String> words = List.of("cat", "dog", "elephant", "bee", "ant", "dolphin");

Map<Integer, List<String>> byLength = words.stream()
    .collect(Collectors.groupingBy(String::length));

byLength.forEach((len, ws) -> System.out.println(len + ": " + ws));

Вывод:

3: [cat, dog, bee, ant]
8: [elephant, dolphin]

Пример 2: Группировка чисел по чётности

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);

Map<String, List<Integer>> byParity = numbers.stream()
    .collect(Collectors.groupingBy(n -> n % 2 == 0 ? "even" : "odd"));

System.out.println(byParity);
// {odd=[1, 3, 5], even=[2, 4, 6]}

Пример 3: Использование partitioningBy для строк

Разделим строки на начинающиеся с "A" и все остальные:

List<String> names = List.of("Alice", "Bob", "Anna", "Charlie");

Map<Boolean, List<String>> byA = names.stream()
    .collect(Collectors.partitioningBy(s -> s.startsWith("A")));

System.out.println("A-names: " + byA.get(true));  // [Alice, Anna]
System.out.println("Other: " + byA.get(false));   // [Bob, Charlie]

5. Полезные нюансы

Как использовать результат группировки

Часто после группировки хочется не просто получить Map, а как-то обработать его:

  • Перебрать все группы и вывести информацию.
  • Найти группу с максимальным количеством элементов.
  • Для каждой группы посчитать, например, сумму или среднее — об этом подробнее в следующей лекции.

Пример: вывести количество сотрудников в каждом отделе

byDepartment.forEach((dept, emps) ->
    System.out.println(dept + ": " + emps.size() + " сотрудников"));

Сравнение с ручной реализацией

Для закрепления: вот как выглядела бы группировка по отделу «по-старинке»:

Map<String, List<Employee>> byDepartment = new HashMap<>();
for (Employee e : employees) {
    String dept = e.getDepartment();
    byDepartment.computeIfAbsent(dept, k -> new ArrayList<>()).add(e);
}

И как с помощью Stream API:

Map<String, List<Employee>> byDepartment = employees.stream()
    .collect(Collectors.groupingBy(Employee::getDepartment));

Вывод: меньше кода, меньше ошибок, читаемость выше.

Лайфхаки

Если нужно сгруппировать не в List, а, например, в Set — используйте вторым параметром Collectors.toSet():

Collectors.groupingBy(Employee::getDepartment, Collectors.toSet())

Можно сразу агрегировать: например, получить количество сотрудников в каждом отделе:

Collectors.groupingBy(Employee::getDepartment, Collectors.counting())

Но об этом — в следующей лекции!

После partitioningBy всегда будет два ключа: true и false. Даже если одна из групп пуста.

После вложенных группировок структура становится «деревом»: Map внутри Map и так далее.

6. Типичные ошибки при группировке и partitioning

Ошибка № 1: Неправильный тип результата. Новички часто ожидают, что результат groupingBy — это просто List<T>, а не Map<K, List<T>>. В результате пытаются вызывать методы списка и получают ошибку компиляции. Помните: группировка — это всегда Map!

Ошибка № 2: NullPointerException при обращении к несуществующей группе. Если вы пытаетесь получить список по несуществующему ключу — получите null. Используйте getOrDefault(key, List.of()) или проверяйте наличие ключа через containsKey.

Ошибка № 3: Использование partitioningBy для задач с несколькими группами. partitioningBy — только для двух групп (true/false). Если групп больше — используйте groupingBy.

Ошибка № 4: Модификация коллекций внутри стрима. Не пытайтесь изменять исходные коллекции или Map внутри стрима — это приведёт к неожиданным ошибкам. Всю обработку делайте через Stream API и коллекторы.

Ошибка № 5: Неочевидная структура вложенных группировок. После вложенного groupingBy результат — Map внутри Map. Не забудьте корректно извлекать данные (например, через getOrDefault), иначе получите ClassCastException.

1
Задача
JAVA 25 SELF, 31 уровень, 2 лекция
Недоступна
Организация команды в корпорации "Инновационные Решения" 🏢
Организация команды в корпорации "Инновационные Решения" 🏢
1
Задача
JAVA 25 SELF, 31 уровень, 2 лекция
Недоступна
Сортировка экзотических фруктов 🍎🍌
Сортировка экзотических фруктов 🍎🍌
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ