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.
Типичные ошибки
- Повторное использование потока — один
Streamможно потребить только один раз. peekдля логики — только отладка; бизнес-логику не размещайте вpeek.- Изменение внешних переменных в лямбде — переменные из замыкания должны быть effectively final.
nullв stream — приведёт к NPE вmap; фильтруйте или используйтеOptional.
Чек-лист по качеству Stream-кода
- Pipeline читается слева направо без "магии" и скрытых побочных эффектов.
- Длинные цепочки вынесены в отдельные методы с говорящими именами.
- Ошибки и пустые случаи обработаны явно (
orElse,orElseThrow,defaultзначение). - Нагрузочные участки проверены профилировщиком, а не интуицией.
Для диагностики под нагрузкой смотрите также асинхронность и диагностику JVM.