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() | В List | List<T> |
toSet() | В Set | Set<T> |
toCollection(supplier) | В указанную коллекцию | C |
toMap(key, value) | В Map | Map<K, V> |
toUnmodifiableList() | В неизменяемый List | List<T> |
toUnmodifiableSet() | В неизменяемый Set | Set<T> |
toUnmodifiableMap() | В неизменяемую Map | Map<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
));
Итоги
- Collectors - мощный инструмент для сбора элементов Stream
- toList/toSet/toMap - базовые коллекторы для сборки в коллекции
- groupingBy - группировка по ключу, поддерживает downstream коллекторы
- partitioningBy - разделение на 2 группы (true/false)
- joining - объединение строк с разделителем
- Агрегирующие коллекторы - counting, summing, averaging, summarizing
- mapping/flatMapping/filtering - преобразование перед сборкой
- collectingAndThen - финальное преобразование результата
- teeing (Java 12+) - объединение двух коллекторов
- Всегда обрабатывай дубликаты в toMap
Задания для практики
-
groupingBy + counting: Дан список слов. Сгруппируй по длине и посчитай количество слов каждой длины.
-
toMap с дубликатами: Дан список
Person(name, city). Создай Map<city, names> где names - строка с именами через запятую. -
partitioningBy + статистика: Дан список оценок (0-100). Раздели на сдавших (>=60) и не сдавших. Для каждой группы выведи статистику (мин, макс, среднее).
-
Многоуровневая группировка: Дан список
Transaction(type, category, amount). Сгруппируй по типу, затем по категории, и посчитай сумму в каждой подгруппе. -
teeing: Дан список чисел. Одним проходом найди сумму положительных и сумму отрицательных чисел.