Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2.3.4. Collectors

toList(), toSet(), toMap(), groupingBy(), partitioningBy(), joining()

Материалы

ТипСсылка
ДокументCollectors
Видеоссылка

Что такое Collector?

Collector - это объект, который описывает как собирать элементы Stream в результат.

// Collector используется с методом collect()
List<String> result = stream.collect(Collectors.toList());

Структура Collector

interface Collector<T, A, R> {
    Supplier<A> supplier();           // Создает контейнер
    BiConsumer<A, T> accumulator();   // Добавляет элемент
    BinaryOperator<A> combiner();     // Объединяет контейнеры (parallel)
    Function<A, R> finisher();        // Преобразует в результат
    Set<Characteristics> characteristics();
}

// T - тип входных элементов
// A - тип промежуточного контейнера
// R - тип результата

Класс Collectors

java.util.stream.Collectors содержит фабричные методы для создания стандартных коллекторов.

import java.util.stream.Collectors;
import static java.util.stream.Collectors.*;  // Статический импорт

Базовые коллекторы

toList() - В List

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

// Collectors.toList() - изменяемый ArrayList
List<String> list = names.stream()
    .filter(n -> n.length() > 3)
    .collect(Collectors.toList());

list.add("Diana");  // OK, можно модифицировать

// Stream.toList() (Java 16+) - неизменяемый список
List<String> immutable = names.stream()
    .filter(n -> n.length() > 3)
    .toList();

immutable.add("Diana");  // UnsupportedOperationException!

toSet() - В Set

// Collectors.toSet() - обычно HashSet
Set<String> set = names.stream()
    .map(String::toLowerCase)
    .collect(Collectors.toSet());

// Конкретная реализация Set
TreeSet<String> treeSet = names.stream()
    .collect(Collectors.toCollection(TreeSet::new));

LinkedHashSet<String> linkedSet = names.stream()
    .collect(Collectors.toCollection(LinkedHashSet::new));

toCollection() - В любую коллекцию

// ArrayList
ArrayList<String> arrayList = names.stream()
    .collect(Collectors.toCollection(ArrayList::new));

// LinkedList
LinkedList<String> linkedList = names.stream()
    .collect(Collectors.toCollection(LinkedList::new));

// TreeSet с компаратором
TreeSet<String> sortedSet = names.stream()
    .collect(Collectors.toCollection(
        () -> new TreeSet<>(Comparator.reverseOrder())
    ));

// ArrayDeque
ArrayDeque<String> deque = names.stream()
    .collect(Collectors.toCollection(ArrayDeque::new));

toUnmodifiableList(), toUnmodifiableSet() (Java 10+)

// Неизменяемый List
List<String> immutableList = names.stream()
    .collect(Collectors.toUnmodifiableList());

// Неизменяемый Set
Set<String> immutableSet = names.stream()
    .collect(Collectors.toUnmodifiableSet());

// Попытка модификации бросит UnsupportedOperationException

toMap() - В Map

Базовый toMap

record Person(int id, String name) {}

List<Person> people = List.of(
    new Person(1, "Alice"),
    new Person(2, "Bob"),
    new Person(3, "Charlie")
);

// id -> Person
Map<Integer, Person> byId = people.stream()
    .collect(Collectors.toMap(
        Person::id,      // keyMapper
        p -> p           // valueMapper (или Function.identity())
    ));
// {1=Person[id=1, name=Alice], 2=Person[id=2, name=Bob], ...}

// id -> name
Map<Integer, String> idToName = people.stream()
    .collect(Collectors.toMap(
        Person::id,
        Person::name
    ));
// {1=Alice, 2=Bob, 3=Charlie}

Обработка дубликатов ключей

List<Person> peopleWithDupes = List.of(
    new Person(1, "Alice"),
    new Person(1, "Alicia"),  // Дубликат id!
    new Person(2, "Bob")
);

// Без mergeFunction - IllegalStateException при дубликате!
// Map<Integer, String> map = peopleWithDupes.stream()
//     .collect(Collectors.toMap(Person::id, Person::name));

// С mergeFunction
Map<Integer, String> map = peopleWithDupes.stream()
    .collect(Collectors.toMap(
        Person::id,
        Person::name,
        (existing, replacement) -> existing  // Оставить первое
    ));
// {1=Alice, 2=Bob}

// Объединить значения
Map<Integer, String> merged = peopleWithDupes.stream()
    .collect(Collectors.toMap(
        Person::id,
        Person::name,
        (v1, v2) -> v1 + ", " + v2  // Объединить через запятую
    ));
// {1=Alice, Alicia, 2=Bob}

Указание типа Map

// LinkedHashMap (сохраняет порядок вставки)
LinkedHashMap<Integer, String> linkedMap = people.stream()
    .collect(Collectors.toMap(
        Person::id,
        Person::name,
        (v1, v2) -> v1,
        LinkedHashMap::new
    ));

// TreeMap (сортировка по ключу)
TreeMap<Integer, String> treeMap = people.stream()
    .collect(Collectors.toMap(
        Person::id,
        Person::name,
        (v1, v2) -> v1,
        TreeMap::new
    ));

// ConcurrentHashMap
ConcurrentHashMap<Integer, String> concurrentMap = people.stream()
    .collect(Collectors.toMap(
        Person::id,
        Person::name,
        (v1, v2) -> v1,
        ConcurrentHashMap::new
    ));

toUnmodifiableMap() (Java 10+)

Map<Integer, String> immutableMap = people.stream()
    .collect(Collectors.toUnmodifiableMap(
        Person::id,
        Person::name
    ));

immutableMap.put(4, "Diana");  // UnsupportedOperationException!

Практические примеры toMap

record Product(String sku, String name, double price) {}

List<Product> products = List.of(
    new Product("SKU001", "Book", 20.0),
    new Product("SKU002", "Laptop", 1000.0),
    new Product("SKU003", "Phone", 500.0)
);

// SKU -> Product (справочник)
Map<String, Product> catalog = products.stream()
    .collect(Collectors.toMap(Product::sku, Function.identity()));

// SKU -> Price (прайс-лист)
Map<String, Double> priceList = products.stream()
    .collect(Collectors.toMap(Product::sku, Product::price));

// Name -> SKU (обратный индекс)
Map<String, String> nameToSku = products.stream()
    .collect(Collectors.toMap(Product::name, Product::sku));

groupingBy() - Группировка

Базовая группировка

record Person(String name, String city, int age) {}

List<Person> people = List.of(
    new Person("Alice", "Moscow", 30),
    new Person("Bob", "SPb", 25),
    new Person("Charlie", "Moscow", 35),
    new Person("Diana", "SPb", 28)
);

// Группировка по городу
Map<String, List<Person>> byCity = people.stream()
    .collect(Collectors.groupingBy(Person::city));
// {Moscow=[Alice, Charlie], SPb=[Bob, Diana]}

// Группировка по возрастной категории
Map<String, List<Person>> byAgeGroup = people.stream()
    .collect(Collectors.groupingBy(p ->
        p.age() < 30 ? "Young" : "Adult"
    ));
// {Young=[Bob, Diana], Adult=[Alice, Charlie]}

groupingBy с downstream collector

// Группировка + подсчет
Map<String, Long> countByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::city,
        Collectors.counting()
    ));
// {Moscow=2, SPb=2}

// Группировка + сумма возрастов
Map<String, Integer> totalAgeByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::city,
        Collectors.summingInt(Person::age)
    ));
// {Moscow=65, SPb=53}

// Группировка + средний возраст
Map<String, Double> avgAgeByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::city,
        Collectors.averagingInt(Person::age)
    ));
// {Moscow=32.5, SPb=26.5}

// Группировка + максимальный возраст
Map<String, Optional<Person>> oldestByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::city,
        Collectors.maxBy(Comparator.comparing(Person::age))
    ));

// Группировка + список имен
Map<String, List<String>> namesByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::city,
        Collectors.mapping(Person::name, Collectors.toList())
    ));
// {Moscow=[Alice, Charlie], SPb=[Bob, Diana]}

// Группировка + объединение имен в строку
Map<String, String> joinedNamesByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::city,
        Collectors.mapping(
            Person::name,
            Collectors.joining(", ")
        )
    ));
// {Moscow=Alice, Charlie, SPb=Bob, Diana}

groupingBy с указанием типа Map

// TreeMap (отсортированные ключи)
TreeMap<String, List<Person>> sortedByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::city,
        TreeMap::new,
        Collectors.toList()
    ));

// LinkedHashMap (порядок вставки)
LinkedHashMap<String, List<Person>> orderedByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::city,
        LinkedHashMap::new,
        Collectors.toList()
    ));

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

record Employee(String name, String dept, String team) {}

List<Employee> employees = List.of(
    new Employee("Alice", "IT", "Backend"),
    new Employee("Bob", "IT", "Frontend"),
    new Employee("Charlie", "HR", "Recruiting"),
    new Employee("Diana", "IT", "Backend")
);

// Группировка по отделу, затем по команде
Map<String, Map<String, List<Employee>>> byDeptAndTeam = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::dept,
        Collectors.groupingBy(Employee::team)
    ));
// {IT={Backend=[Alice, Diana], Frontend=[Bob]}, HR={Recruiting=[Charlie]}}

// Группировка по отделу, затем подсчет по команде
Map<String, Map<String, Long>> countByDeptAndTeam = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::dept,
        Collectors.groupingBy(
            Employee::team,
            Collectors.counting()
        )
    ));
// {IT={Backend=2, Frontend=1}, HR={Recruiting=1}}

groupingByConcurrent() - Параллельная группировка

// Для parallel streams - потокобезопасная группировка
ConcurrentMap<String, List<Person>> concurrentByCity = people.parallelStream()
    .collect(Collectors.groupingByConcurrent(Person::city));

partitioningBy() - Разделение на две группы

Базовое разделение

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

// Разделение на четные и нечетные
Map<Boolean, List<Integer>> evenOdd = numbers.stream()
    .collect(Collectors.partitioningBy(n -> n % 2 == 0));
// {false=[1, 3, 5, 7, 9], true=[2, 4, 6, 8, 10]}

List<Integer> evens = evenOdd.get(true);   // [2, 4, 6, 8, 10]
List<Integer> odds = evenOdd.get(false);   // [1, 3, 5, 7, 9]

partitioningBy с downstream collector

// Разделение + подсчет
Map<Boolean, Long> countEvenOdd = numbers.stream()
    .collect(Collectors.partitioningBy(
        n -> n % 2 == 0,
        Collectors.counting()
    ));
// {false=5, true=5}

// Разделение + сумма
Map<Boolean, Integer> sumEvenOdd = numbers.stream()
    .collect(Collectors.partitioningBy(
        n -> n % 2 == 0,
        Collectors.summingInt(Integer::intValue)
    ));
// {false=25, true=30}

Практические примеры partitioningBy

record Student(String name, int score) {}

List<Student> students = List.of(
    new Student("Alice", 85),
    new Student("Bob", 45),
    new Student("Charlie", 90),
    new Student("Diana", 55)
);

// Разделение на сдавших и не сдавших
Map<Boolean, List<Student>> passedFailed = students.stream()
    .collect(Collectors.partitioningBy(s -> s.score() >= 60));

List<Student> passed = passedFailed.get(true);   // [Alice, Charlie]
List<Student> failed = passedFailed.get(false);  // [Bob, Diana]

// Разделение + имена
Map<Boolean, List<String>> passedFailedNames = students.stream()
    .collect(Collectors.partitioningBy(
        s -> s.score() >= 60,
        Collectors.mapping(Student::name, Collectors.toList())
    ));
// {false=[Bob, Diana], true=[Alice, Charlie]}

groupingBy vs partitioningBy

// groupingBy - произвольное количество групп
Map<String, List<Person>> byCity = people.stream()
    .collect(Collectors.groupingBy(Person::city));
// Может быть 0, 1, 2, ... групп

// partitioningBy - ровно 2 группы (true/false)
Map<Boolean, List<Person>> adults = people.stream()
    .collect(Collectors.partitioningBy(p -> p.age() >= 18));
// Всегда 2 ключа: true и false (даже если список пустой)

joining() - Объединение строк

Базовое объединение

List<String> words = List.of("Hello", "World", "Java");

// Простое объединение
String joined = words.stream()
    .collect(Collectors.joining());
// "HelloWorldJava"

// С разделителем
String csv = words.stream()
    .collect(Collectors.joining(", "));
// "Hello, World, Java"

// С разделителем, префиксом и суффиксом
String json = words.stream()
    .collect(Collectors.joining(", ", "[", "]"));
// "[Hello, World, Java]"

Практические примеры joining

record Person(String name, int age) {}

List<Person> people = List.of(
    new Person("Alice", 30),
    new Person("Bob", 25)
);

// Список имен через запятую
String names = people.stream()
    .map(Person::name)
    .collect(Collectors.joining(", "));
// "Alice, Bob"

// SQL IN clause
String sqlIn = people.stream()
    .map(p -> "'" + p.name() + "'")
    .collect(Collectors.joining(", ", "(", ")"));
// "('Alice', 'Bob')"

// HTML список
String htmlList = people.stream()
    .map(p -> "<li>" + p.name() + "</li>")
    .collect(Collectors.joining("\n", "<ul>\n", "\n</ul>"));
// <ul>
// <li>Alice</li>
// <li>Bob</li>
// </ul>

// CSV строка
String csvLine = Stream.of("Alice", "30", "Moscow")
    .collect(Collectors.joining(","));
// "Alice,30,Moscow"

Агрегирующие коллекторы

counting() - Подсчет

long count = people.stream()
    .collect(Collectors.counting());

// Обычно используется как downstream
Map<String, Long> countByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::city,
        Collectors.counting()
    ));

summingInt(), summingLong(), summingDouble()

// Сумма возрастов
int totalAge = people.stream()
    .collect(Collectors.summingInt(Person::age));

// Сумма цен
double totalPrice = products.stream()
    .collect(Collectors.summingDouble(Product::price));

// С группировкой
Map<String, Integer> totalAgeByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::city,
        Collectors.summingInt(Person::age)
    ));

averagingInt(), averagingLong(), averagingDouble()

// Средний возраст
double avgAge = people.stream()
    .collect(Collectors.averagingInt(Person::age));

// С группировкой
Map<String, Double> avgAgeByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::city,
        Collectors.averagingInt(Person::age)
    ));

summarizingInt(), summarizingLong(), summarizingDouble()

// Полная статистика
IntSummaryStatistics stats = people.stream()
    .collect(Collectors.summarizingInt(Person::age));

stats.getCount();    // количество
stats.getSum();      // сумма
stats.getMin();      // минимум
stats.getMax();      // максимум
stats.getAverage();  // среднее

// С группировкой
Map<String, IntSummaryStatistics> statsByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::city,
        Collectors.summarizingInt(Person::age)
    ));

maxBy(), minBy()

// Максимальный по возрасту
Optional<Person> oldest = people.stream()
    .collect(Collectors.maxBy(Comparator.comparing(Person::age)));

// Минимальный по возрасту
Optional<Person> youngest = people.stream()
    .collect(Collectors.minBy(Comparator.comparing(Person::age)));

// С группировкой
Map<String, Optional<Person>> oldestByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::city,
        Collectors.maxBy(Comparator.comparing(Person::age))
    ));

Преобразующие коллекторы

mapping() - Преобразование перед сборкой

// Собрать только имена
List<String> names = people.stream()
    .collect(Collectors.mapping(
        Person::name,
        Collectors.toList()
    ));

// То же самое проще:
List<String> names = people.stream()
    .map(Person::name)
    .toList();

// Но mapping() полезен как downstream collector
Map<String, List<String>> namesByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::city,
        Collectors.mapping(Person::name, Collectors.toList())
    ));

flatMapping() (Java 9+)

record Order(String id, List<String> items) {}

List<Order> orders = List.of(
    new Order("1", List.of("Book", "Pen")),
    new Order("2", List.of("Laptop")),
    new Order("3", List.of("Mouse", "Keyboard"))
);

// Все товары (плоский список)
Set<String> allItems = orders.stream()
    .collect(Collectors.flatMapping(
        order -> order.items().stream(),
        Collectors.toSet()
    ));
// [Book, Pen, Laptop, Mouse, Keyboard]

// С группировкой - все товары по какому-то признаку
Map<Integer, Set<String>> itemsByOrderLength = orders.stream()
    .collect(Collectors.groupingBy(
        o -> o.items().size(),
        Collectors.flatMapping(
            o -> o.items().stream(),
            Collectors.toSet()
        )
    ));

filtering() (Java 9+)

// Фильтрация внутри группы
Map<String, List<Person>> adultsByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::city,
        Collectors.filtering(
            p -> p.age() >= 18,
            Collectors.toList()
        )
    ));

// Отличие от filter() перед groupingBy:
// - filtering() сохраняет все группы (даже пустые)
// - filter() удаляет элементы ДО группировки

collectingAndThen() - Финальное преобразование

// Собрать в List, затем сделать неизменяемым
List<String> immutableNames = people.stream()
    .map(Person::name)
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),
        Collections::unmodifiableList
    ));

// Собрать в List, затем получить размер
int count = people.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),
        List::size
    ));

// Собрать максимум, затем извлечь из Optional
Person oldest = people.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.maxBy(Comparator.comparing(Person::age)),
        opt -> opt.orElseThrow()
    ));

reducing() - Свертка

Базовый reducing

// Сумма возрастов
Optional<Integer> totalAge = people.stream()
    .map(Person::age)
    .collect(Collectors.reducing(Integer::sum));

// С identity
int totalAge = people.stream()
    .map(Person::age)
    .collect(Collectors.reducing(0, Integer::sum));

// С mapper
int totalAge = people.stream()
    .collect(Collectors.reducing(
        0,                    // identity
        Person::age,          // mapper
        Integer::sum          // reducer
    ));

reducing vs reduce

// Stream.reduce() - терминальная операция
int sum1 = numbers.stream()
    .reduce(0, Integer::sum);

// Collectors.reducing() - коллектор (для downstream)
Map<String, Integer> totalAgeByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::city,
        Collectors.reducing(0, Person::age, Integer::sum)
    ));

teeing() (Java 12+) - Объединение двух коллекторов

Базовый teeing

// Одновременно найти мин и макс
record MinMax(Integer min, Integer max) {}

MinMax result = numbers.stream()
    .collect(Collectors.teeing(
        Collectors.minBy(Integer::compareTo),
        Collectors.maxBy(Integer::compareTo),
        (min, max) -> new MinMax(
            min.orElse(null),
            max.orElse(null)
        )
    ));

Практические примеры teeing

// Сумма и количество одновременно
record SumCount(int sum, long count) {}

SumCount result = numbers.stream()
    .collect(Collectors.teeing(
        Collectors.summingInt(Integer::intValue),
        Collectors.counting(),
        SumCount::new
    ));

// Вычисление среднего вручную
double average = numbers.stream()
    .collect(Collectors.teeing(
        Collectors.summingInt(Integer::intValue),
        Collectors.counting(),
        (sum, count) -> count == 0 ? 0 : (double) sum / count
    ));

// Разделение на две категории с подсчетом
record PassFailCount(long passed, long failed) {}

PassFailCount counts = students.stream()
    .collect(Collectors.teeing(
        Collectors.filtering(s -> s.score() >= 60, Collectors.counting()),
        Collectors.filtering(s -> s.score() < 60, Collectors.counting()),
        PassFailCount::new
    ));

Создание кастомного Collector

Простой кастомный коллектор

// Коллектор для сборки в ImmutableList (Guava)
Collector<String, ?, ImmutableList<String>> toImmutableList =
    Collector.of(
        ImmutableList::<String>builder,     // supplier
        ImmutableList.Builder::add,          // accumulator
        (b1, b2) -> b1.addAll(b2.build()),   // combiner
        ImmutableList.Builder::build         // finisher
    );

ImmutableList<String> result = names.stream()
    .collect(toImmutableList);

Коллектор для StringBuilder

Collector<String, StringBuilder, String> joining =
    Collector.of(
        StringBuilder::new,
        StringBuilder::append,
        StringBuilder::append,
        StringBuilder::toString
    );

String result = Stream.of("a", "b", "c")
    .collect(joining);  // "abc"

Коллектор с характеристиками

Collector<String, ?, Set<String>> toUnorderedSet =
    Collector.of(
        HashSet::new,
        Set::add,
        (s1, s2) -> { s1.addAll(s2); return s1; },
        Collector.Characteristics.UNORDERED,
        Collector.Characteristics.IDENTITY_FINISH
    );

Сводная таблица Collectors

CollectorОписаниеРезультат
toList()В ListList<T>
toSet()В SetSet<T>
toCollection(supplier)В указанную коллекциюC
toMap(key, value)В MapMap<K, V>
toUnmodifiableList()В неизменяемый ListList<T>
toUnmodifiableSet()В неизменяемый SetSet<T>
toUnmodifiableMap()В неизменяемую MapMap<K, V>
groupingBy(classifier)ГруппировкаMap<K, List<T>>
partitioningBy(predicate)Разделение на 2 группыMap<Boolean, List<T>>
joining()Объединение строкString
counting()ПодсчетLong
summingInt/Long/Double()Суммаint/long/double
averagingInt/Long/Double()СреднееDouble
summarizingInt/Long/Double()Статистика*SummaryStatistics
maxBy(comparator)МаксимумOptional<T>
minBy(comparator)МинимумOptional<T>
mapping(mapper, downstream)Преобразованиезависит от downstream
flatMapping(mapper, downstream)Выравниваниезависит от downstream
filtering(predicate, downstream)Фильтрациязависит от downstream
collectingAndThen(collector, finisher)Финализациязависит от finisher
reducing()СверткаOptional<T> или T
teeing(c1, c2, merger)Объединение коллекторовзависит от merger

Best Practices

Выбор коллектора

// Для простого списка - toList() или Stream.toList()
List<String> list = stream.toList();  // Java 16+, неизменяемый

// Для изменяемого списка
List<String> mutableList = stream.collect(Collectors.toList());

// Для конкретной реализации
TreeSet<String> sorted = stream
    .collect(Collectors.toCollection(TreeSet::new));

Избегай лишних операций

// Плохо - лишний map
Map<String, Integer> ages = people.stream()
    .collect(Collectors.toMap(
        p -> p.name(),
        p -> p.age()
    ));

// Хорошо - method reference
Map<String, Integer> ages = people.stream()
    .collect(Collectors.toMap(Person::name, Person::age));

Обработка дубликатов в toMap

// Всегда думай о дубликатах!
// Плохо - бросит исключение при дубликате
Map<String, Person> byName = people.stream()
    .collect(Collectors.toMap(Person::name, p -> p));

// Хорошо - явная обработка
Map<String, Person> byName = people.stream()
    .collect(Collectors.toMap(
        Person::name,
        Function.identity(),
        (existing, replacement) -> existing
    ));

Итоги

  1. Collectors - мощный инструмент для сбора элементов Stream
  2. toList/toSet/toMap - базовые коллекторы для сборки в коллекции
  3. groupingBy - группировка по ключу, поддерживает downstream коллекторы
  4. partitioningBy - разделение на 2 группы (true/false)
  5. joining - объединение строк с разделителем
  6. Агрегирующие коллекторы - counting, summing, averaging, summarizing
  7. mapping/flatMapping/filtering - преобразование перед сборкой
  8. collectingAndThen - финальное преобразование результата
  9. teeing (Java 12+) - объединение двух коллекторов
  10. Всегда обрабатывай дубликаты в toMap

Задания для практики

  1. groupingBy + counting: Дан список слов. Сгруппируй по длине и посчитай количество слов каждой длины.

  2. toMap с дубликатами: Дан список Person(name, city). Создай Map<city, names> где names - строка с именами через запятую.

  3. partitioningBy + статистика: Дан список оценок (0-100). Раздели на сдавших (>=60) и не сдавших. Для каждой группы выведи статистику (мин, макс, среднее).

  4. Многоуровневая группировка: Дан список Transaction(type, category, amount). Сгруппируй по типу, затем по категории, и посчитай сумму в каждой подгруппе.

  5. teeing: Дан список чисел. Одним проходом найди сумму положительных и сумму отрицательных чисел.