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 лекция
Недоступна
Сортировка экзотических фруктов 🍎🍌
Сортировка экзотических фруктов 🍎🍌
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Timofey Novitsky Уровень 36
21 мая 2026
Уверен, что даже опытные держатся от этого подальше, чтобы их код можно было поддерживать. Нечитаемая жуть.
Riga Уровень 37
24 апреля 2026
Пример 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] правильный вывод 3: [cat, dog, bee, ant] 7: [dolphin] 8: [elephant, dolphin]