Терминальные операции

Терминальная операция запускает pipeline Stream API и завершает stream. Результатом может быть значение, коллекция, Optional или только побочный эффект.

Группы операций

ЗадачаМетоды
количествоcount
поискfindFirst, findAny
проверкаanyMatch, allMatch, noneMatch
экстремумmin, max
свёрткаreduce
накоплениеcollect, toList
обходforEach, forEachOrdered
массивtoArray

find... и match-операции могут завершиться раньше полного обхода. findFirst уважает encounter order, а findAny оставляет больше свободы параллельному выполнению.

Optional как результат

Optional<User> oldest = users.stream()
        .max(Comparator.comparingInt(User::age));
 
String name = oldest.map(User::name).orElse("нет данных");

min, max, findFirst, findAny возвращают Optional, потому что stream может быть пустым.

Reduce

int sum = numbers.stream().reduce(0, Integer::sum);

Identity должен быть нейтральным элементом, а accumulator — ассоциативным, если pipeline может стать parallel. Ошибочная identity и неассоциативная операция дают разные результаты при разном разбиении.

Для построения изменяемой коллекции обычно нужен collect, а не reduce:

Map<String, Long> counts = words.stream()
        .collect(Collectors.groupingBy(
                String::toLowerCase,
                Collectors.counting()));

Полезные collectors

CollectorНазначение
toList, toSetсобрать элементы
joiningсоединить строки
groupingByсгруппировать по ключу
partitioningByразбить по boolean-условию
mapping, filteringdownstream-преобразование
summarizingIntcount, sum, min, average, max
toMapпостроить map

При повторяющемся ключе двухаргументный toMap(key, value) бросает IllegalStateException. Правило объединения задают третьим аргументом:

Map<String, User> byEmail = users.stream()
        .collect(Collectors.toMap(
                User::email,
                Function.identity(),
                (first, duplicate) -> first));

Stream.toList() возвращает неизменяемый список. Конкретный тип результата Collectors.toList() не гарантирован, и изменяемость не является частью его контракта; если нужен ArrayList, используется toCollection(ArrayList::new).

ForEach и порядок

forEach в parallel stream не сохраняет encounter order. forEachOrdered сохраняет, но может уменьшить параллелизм. Для вычисления результата предпочтительнее collect/reduce, чем запись в общий mutable список.

Промежуточная часть pipeline: Промежуточные операции.

Вопросы для собеседования

  1. Что происходит со stream после terminal-операции? Он считается потреблённым.
  2. Чем findFirst отличается от findAny? Первый сохраняет порядок, второй допускает любой элемент.
  3. Почему reduce требует ассоциативности? Частичные результаты могут объединяться в разном порядке.
  4. Что случится с дубликатом ключа в простом toMap? Исключение.
  5. Когда collect лучше reduce? При накоплении в mutable container.
  6. Изменяем ли результат Stream.toList? Нет.
  7. Сохраняет ли parallel forEach порядок? Нет; для этого есть forEachOrdered.