Перейти к основному содержимому

Stream API в Java

Разработчику Архитектору

Stream API

Stream API (пакет java.util.stream, с Java 8) — способ описать что сделать с последовательностью элементов, не переписывая циклы вручную. Поток не хранит данные — он читает источник (коллекцию, массив, генератор) и прогоняет элементы через цепочку операций.

Базовые материалы: коллекции, особенности языка (лямбды), справочник.

Если нужен формат "полный API", смотрите разделы Streams и Gatherers — API-дополнение в основном справочнике.


Поток и pipeline

Play ITЗагрузка интерактивного демо…

Операции делятся на:

ТипПримерыРезультат
Промежуточные (lazy)filter, map, flatMap, sorted, distinct, limitНовый Stream
Терминальныеcollect, forEach, reduce, count, findFirst, anyMatchЗначение или void

Пока не вызвана терминальная операция, вычислений нет — это ленивое выполнение.

List<String> active = users.stream()
.filter(User::isActive)
.map(User::getEmail)
.sorted()
.toList(); // Java 16+: неизменяемый список

Разбор:

  • фрагмент показывает прикладной сценарий по теме раздела: здесь есть входные данные, операции над ними и проверяемый итог.
  • ключевые вызовы stream, filter, map, sorted, toList задают основную механику этого примера: обработку, проверку и формирование результата.
  • в stream-цепочке операции применяются последовательно к каждому элементу, а терминальный вызов (collect/toList) материализует итоговую коллекцию.
  • после запуска стоит проверить именно эффект этого примера — вывод, значение переменной, статус ответа, состояние БД или результат теста.
  • при переносе в production обычно добавляют валидацию входа, явную обработку ошибок и тест на граничные случаи вокруг этого сценария.

Эквивалент с collect:

List<String> active = users.stream()
.filter(User::isActive)
.map(User::getEmail)
.sorted()
.collect(Collectors.toList());

Разбор:

  • фрагмент показывает прикладной сценарий по теме раздела: здесь есть входные данные, операции над ними и проверяемый итог.
  • ключевые вызовы stream, filter, map, sorted, collect, toList задают основную механику этого примера: обработку, проверку и формирование результата.
  • в stream-цепочке операции применяются последовательно к каждому элементу, а терминальный вызов (collect/toList) материализует итоговую коллекцию.
  • после запуска стоит проверить именно эффект этого примера — вывод, значение переменной, статус ответа, состояние БД или результат теста.
  • при переносе в production обычно добавляют валидацию входа, явную обработку ошибок и тест на граничные случаи вокруг этого сценария.

Источники потока

List<String> fromList = List.of("a", "b").stream().toList();

IntStream.range(0, 10).forEach(System.out::println);

Stream<String> lines = Files.lines(Path.of("data.txt")); // закрывать try-with-resources

Stream.iterate(0, n -> n + 1).limit(5);

Разбор:

  • фрагмент показывает прикладной сценарий по теме раздела: здесь есть входные данные, операции над ними и проверяемый итог.
  • ключевые вызовы of, stream, toList, range, forEach, lines, iterate задают основную механику этого примера: обработку, проверку и формирование результата.
  • в stream-цепочке операции применяются последовательно к каждому элементу, а терминальный вызов (collect/toList) материализует итоговую коллекцию.
  • после запуска стоит проверить именно эффект этого примера — вывод, значение переменной, статус ответа, состояние БД или результат теста.
  • при переносе в production обычно добавляют валидацию входа, явную обработку ошибок и тест на граничные случаи вокруг этого сценария.

Для примитивов есть IntStream, LongStream, DoubleStream — меньше boxing и выше скорость на больших объёмах.


Частые операции

filter — отбор по условию.

map — преобразование элемента.

flatMap — "разворот" вложенных структур (список списков → один список).

List<String> tokens = sentences.stream()
.flatMap(s -> Arrays.stream(s.split("\\s+")))
.map(String::toLowerCase)
.distinct()
.toList();

Разбор:

  • фрагмент показывает прикладной сценарий по теме раздела: здесь есть входные данные, операции над ними и проверяемый итог.
  • ключевые вызовы stream, flatMap, split, map, distinct, toList задают основную механику этого примера: обработку, проверку и формирование результата.
  • в stream-цепочке операции применяются последовательно к каждому элементу, а терминальный вызов (collect/toList) материализует итоговую коллекцию.
  • после запуска стоит проверить именно эффект этого примера — вывод, значение переменной, статус ответа, состояние БД или результат теста.
  • при переносе в production обычно добавляют валидацию входа, явную обработку ошибок и тест на граничные случаи вокруг этого сценария.

reduce — свёртка к одному значению:

int sum = numbers.stream().mapToInt(Integer::intValue).sum();
// или
Optional<Integer> opt = numbers.stream().reduce(Integer::sum);

Разбор:

  • фрагмент показывает прикладной сценарий по теме раздела: здесь есть входные данные, операции над ними и проверяемый итог.
  • ключевые вызовы stream, mapToInt, sum, reduce задают основную механику этого примера: обработку, проверку и формирование результата.
  • в stream-цепочке операции применяются последовательно к каждому элементу, а терминальный вызов (collect/toList) материализует итоговую коллекцию.
  • после запуска стоит проверить именно эффект этого примера — вывод, значение переменной, статус ответа, состояние БД или результат теста.
  • при переносе в production обычно добавляют валидацию входа, явную обработку ошибок и тест на граничные случаи вокруг этого сценария.

Операции Stream и коллекции — быстрый ориентир

ЗадачаStream-решениеЧто на выходе
Отобрать элементыfilter(...)Stream<T>
Преобразовать типmap(...)Stream<R>
Развернуть вложенные коллекцииflatMap(...)Stream<R>
Получить списокtoList()List<T>
Сгруппироватьcollect(groupingBy(...))Map<K, List<T>>

Список полезно читать вместе с коллекциями в Java, чтобы видеть, как pipeline превращается обратно в List, Set или Map.


Collectors

Collectors группирует результаты сложнее, чем toList:

Map<String, List<User>> byCity = users.stream()
.collect(Collectors.groupingBy(User::getCity));

String names = users.stream()
.map(User::getName)
.collect(Collectors.joining(", "));

Double avgAge = users.stream()
.collect(Collectors.averagingInt(User::getAge));

Разбор:

  • фрагмент показывает прикладной сценарий по теме раздела: здесь есть входные данные, операции над ними и проверяемый итог.
  • ключевые вызовы stream, collect, groupingBy, map, joining, averagingInt задают основную механику этого примера: обработку, проверку и формирование результата.
  • в stream-цепочке операции применяются последовательно к каждому элементу, а терминальный вызов (collect/toList) материализует итоговую коллекцию.
  • после запуска стоит проверить именно эффект этого примера — вывод, значение переменной, статус ответа, состояние БД или результат теста.
  • при переносе в production обычно добавляют валидацию входа, явную обработку ошибок и тест на граничные случаи вокруг этого сценария.

Для неизменяемых коллекций в новом коде предпочтительны терминальные методы потока — toList(), toSet(), toMap() (Java 10+ / 16+ с оговорками по дубликатам ключей).


Практический сценарий — агрегация заказов

Map<String, BigDecimal> totalByCity = orders.stream()
.filter(o -> o.status() == Status.PAID)
.collect(Collectors.groupingBy(
Order::city,
Collectors.mapping(
Order::amount,
Collectors.reducing(BigDecimal.ZERO, BigDecimal::add)
)
));

Разбор:

  • фрагмент показывает прикладной сценарий по теме раздела: здесь есть входные данные, операции над ними и проверяемый итог.
  • ключевые вызовы stream, filter, status, collect, groupingBy, mapping, reducing задают основную механику этого примера: обработку, проверку и формирование результата.
  • в stream-цепочке операции применяются последовательно к каждому элементу, а терминальный вызов (collect/toList) материализует итоговую коллекцию.
  • после запуска стоит проверить именно эффект этого примера — вывод, значение переменной, статус ответа, состояние БД или результат теста.
  • при переносе в production обычно добавляют валидацию входа, явную обработку ошибок и тест на граничные случаи вокруг этого сценария.

Этот паттерн часто встречается в отчетах и dashboard-сервисах: фильтрация, группировка и свертка в одном pipeline.


Optional

Optional<T> — контейнер "значение есть или нет", часто возвращается findFirst, reduce, max.

Optional<User> first = users.stream()
.filter(u -> u.getRole().equals("ADMIN"))
.findFirst();

String name = first.map(User::getName).orElse("unknown");

Разбор:

  • фрагмент показывает прикладной сценарий по теме раздела: здесь есть входные данные, операции над ними и проверяемый итог.
  • ключевые вызовы stream, filter, getRole, equals, findFirst, map, orElse задают основную механику этого примера: обработку, проверку и формирование результата.
  • в stream-цепочке операции применяются последовательно к каждому элементу, а терминальный вызов (collect/toList) материализует итоговую коллекцию.
  • после запуска стоит проверить именно эффект этого примера — вывод, значение переменной, статус ответа, состояние БД или результат теста.
  • при переносе в production обычно добавляют валидацию входа, явную обработку ошибок и тест на граничные случаи вокруг этого сценария.

Не используйте Optional в полях сущностей и не передавайте null в Optional.of — только Optional.ofNullable.


Ленивость и стоимость операций

Порядок шагов в pipeline влияет на производительность:

  • сначала ставьте filter, чтобы как можно раньше сократить объем данных;
  • limit полезно поднимать ближе к началу цепочки;
  • sorted дорогой, потому что требует материализовать и отсортировать элементы.

Пример:

List<User> top = users.stream()
.filter(User::isActive)
.limit(100)
.sorted(Comparator.comparing(User::getScore).reversed())
.toList();

Разбор:

  • фрагмент показывает прикладной сценарий по теме раздела: здесь есть входные данные, операции над ними и проверяемый итог.
  • ключевые вызовы stream, filter, limit, sorted, comparing, reversed, toList задают основную механику этого примера: обработку, проверку и формирование результата.
  • в stream-цепочке операции применяются последовательно к каждому элементу, а терминальный вызов (collect/toList) материализует итоговую коллекцию.
  • после запуска стоит проверить именно эффект этого примера — вывод, значение переменной, статус ответа, состояние БД или результат теста.
  • при переносе в production обычно добавляют валидацию входа, явную обработку ошибок и тест на граничные случаи вокруг этого сценария.

Функциональные интерфейы

ИнтерфейсМетодНазначение
Predicate<T>testУсловие (filter)
Function<T,R>applyПреобразование (map)
Consumer<T>acceptПобочное действие (forEach)
Supplier<T>getПоставка значения (supplyAsync)

Ссылка на метод (User::getName) — сокращение для лямбды, когда передаётся существующий метод.


Параллельные потоки

collection.parallelStream() делит работу по нескольким потокам ОС.

Имеет смысл при:

  • большом объёме данных;
  • "тяжёлой" чистой функции в map/filter;
  • отсутствии общей изменяемой состояния.

Риски:

  • порядок элементов не гарантирован без forOrdered;
  • гонки при записи в общие структуры;
  • на малых списках часто медленнее из-за накладных расходов.

Для I/O-bound задач лучше асинхронные API (CompletableFuture, virtual threads), а не parallelStream.


Stream или обычный цикл?

СитуацияРекомендация
Читаемая цепочка преобразованийStream
Несколько break/continue, сложная логикаfor
Примитивы, горячий участок кодаfor или IntStream
Побочные эффекты в каждой итерацииЯвный цикл; в stream — только forEach с осторожностью

Stream не заменяет коллекции: после collect вы снова работаете с List/Map.


Типичные ошибки

  1. Повторное использование потока — один Stream можно потребить только один раз.
  2. peek для логики — только отладка; бизнес-логику не размещайте в peek.
  3. Изменение внешних переменных в лямбде — переменные из замыкания должны быть effectively final.
  4. null в stream — приведёт к NPE в map; фильтруйте или используйте Optional.

Чек-лист по качеству Stream-кода

  • Pipeline читается слева направо без "магии" и скрытых побочных эффектов.
  • Длинные цепочки вынесены в отдельные методы с говорящими именами.
  • Ошибки и пустые случаи обработаны явно (orElse, orElseThrow, default значение).
  • Нагрузочные участки проверены профилировщиком, а не интуицией.

Для диагностики под нагрузкой смотрите также асинхронность и диагностику JVM.


Связанные материалы