Collectors
Роль
Collector<T,A,R> описывает изменяемое свёртывание элементов Stream:
T— входной элемент;A— внутренний аккумулятор;R— итог.
Collectors предоставляет готовые фабрики таких свёрток.
Map<String, List<Order>> ordersByStatus = orders.stream()
.collect(Collectors.groupingBy(Order::status));Терминальная операция collect запускает конвейер. Collector знает, как создать аккумулятор, добавить элемент, объединить частичные результаты и получить итог.
Механика Collector
У коллектора есть:
supplier— создаёт пустой аккумулятор;accumulator— добавляет один элемент;combiner— объединяет два аккумулятора;finisher— превращает аккумулятор в результат;- characteristics — сообщает свойства, например
IDENTITY_FINISH,UNORDERED,CONCURRENT.
Combiner особенно важен для параллельного потока. Операция объединения должна быть ассоциативной, а части не должны повреждать общее mutable-состояние.
Собственный collector для простого случая:
Collector<String, StringJoiner, String> csvCollector =
Collector.of(
() -> new StringJoiner(","),
StringJoiner::add,
StringJoiner::merge,
StringJoiner::toString
);Сборка коллекций
List<String> names = users.stream()
.map(User::name)
.collect(Collectors.toList());
Set<String> unique = users.stream()
.map(User::name)
.collect(Collectors.toSet());
ArrayList<String> concrete = users.stream()
.map(User::name)
.collect(Collectors.toCollection(ArrayList::new));Collectors.toList() не обещает конкретный класс, mutability или serializability результата. Если нужен конкретный контракт, используйте toCollection. toUnmodifiableList возвращает немодифицируемый результат и не допускает null.
Stream.toList() — отдельный терминальный метод, а не collector; он возвращает немодифицируемый список и может отличаться от Collectors.toList().
toMap и дубликаты
Map<Long, User> byId = users.stream()
.collect(Collectors.toMap(
User::id,
Function.identity()
));Повторный ключ без merge-функции приводит к IllegalStateException. Политику нужно выбрать явно:
Map<String, User> latestByEmail = users.stream()
.collect(Collectors.toMap(
User::email,
Function.identity(),
(left, right) -> right,
LinkedHashMap::new
));«Оставить последний» корректно только если порядок определён и является частью требования.
Группировка и downstream collector
Map<String, Long> countByStatus = orders.stream()
.collect(Collectors.groupingBy(
Order::status,
Collectors.counting()
));
Map<String, Set<Long>> idsByStatus = orders.stream()
.collect(Collectors.groupingBy(
Order::status,
Collectors.mapping(
Order::id,
Collectors.toSet()
)
));groupingBy группирует по произвольному ключу. partitioningBy всегда создаёт две логические группы по boolean-предикату:
Map<Boolean, List<Order>> partition = orders.stream()
.collect(Collectors.partitioningBy(Order::paid));Строки, числа, финальное преобразование
String joined = names.stream()
.collect(Collectors.joining(", ", "[", "]"));
IntSummaryStatistics stats = orders.stream()
.collect(Collectors.summarizingInt(Order::itemCount));
List<String> immutable = names.stream()
.collect(Collectors.collectingAndThen(
Collectors.toList(),
List::copyOf
));Для чисел также есть summingInt, averagingLong, maxBy, minBy, reducing.
Ловушки
- Использовать
toMapбез политики повторных ключей. - Предполагать, что
Collectors.toList()вернулArrayListили неизменяемый список. - Путать
groupingByиpartitioningBy. - Писать collector с неассоциативным combiner.
- Захватывать внешнюю mutable-коллекцию в accumulator параллельного потока.
- Делать сложную вложенную группировку, когда явный цикл легче проверить.
Вопросы на собеседовании
- За что отвечают supplier, accumulator, combiner и finisher?
- Что произойдёт при повторном ключе в
toMap? - Чем
groupingByотличается отpartitioningBy? - Какие гарантии даёт
Collectors.toList()? - Чем
Stream.toList()отличается отCollectors.toList()? - Почему combiner должен быть ассоциативным?
Связанные заметки
Stream API · Терминальные операции · ФИ стандартной библиотеки Java · Map · List · Set