Тема 10. Stream API, Optional
Stream API и Optional решают две повседневные задачи:
- Понятно описывать обработку коллекций как конвейер шагов.
- Явно работать со сценарием «значение может отсутствовать».
Stream API: идея и модель
Stream это не коллекция, а последовательность вычислений над данными.
Ключевые свойства:
- не хранит элементы сам по себе;
- обычно не изменяет исходную коллекцию;
- промежуточные операции ленивые;
- запуск происходит на терминальной операции.
Из чего состоит stream-конвейер
Source(источник):list.stream(),Stream.of(...),Files.lines(...).Intermediate operations:filter,map,flatMap,sorted,distinct,limit.Terminal operation:toList,collect,reduce,count,forEach,findFirst.
Пример 1. Фильтрация и преобразование
List<String> names = List.of("ann", "bob", "alex", "anna");
List<String> result = names.stream()
.filter(n -> n.length() > 3)
.map(String::toUpperCase)
.sorted()
.toList();
System.out.println(result); // [ALEX, ANNA]
Пример 2. Агрегация (reduce и Collectors)
List<Integer> prices = List.of(100, 250, 80, 70);
int total = prices.stream()
.reduce(0, Integer::sum);
double avg = prices.stream()
.collect(java.util.stream.Collectors.averagingInt(Integer::intValue));
System.out.println(total); // 500
System.out.println(avg); // 125.0
Пример 3. flatMap для вложенных структур
List<List<String>> tags = List.of(
List.of("java", "backend"),
List.of("backend", "spring")
);
List<String> unique = tags.stream()
.flatMap(List::stream)
.distinct()
.sorted()
.toList();
System.out.println(unique); // [backend, java, spring]
Ленивая природа Stream
Stream<String> s = names.stream().filter(n -> n.startsWith("a"));
// Пока нет терминальной операции, фильтрация фактически не исполняется
long count = s.count();
Эта модель полезна для производительности и позволяет JVM оптимизировать пайплайн.
Когда Stream API уместен, а когда лучше цикл
Уместен:
- Трансформация данных в несколько шагов.
- Фильтрация + агрегация.
- Декларативный код без сложных побочных эффектов.
Лучше цикл:
- Сложная ветвистая бизнес-логика с состоянием.
- Нужна тонкая отладка на каждом шаге.
- Критичный hot path, где простой цикл понятнее и быстрее.
Параллельные стримы (parallelStream)
parallelStream() может ускорить CPU-bound обработку, но подходит не всегда.
Используйте осторожно:
- лямбды должны быть без shared mutable state;
- накладные расходы на распараллеливание могут съесть выгоду;
- в I/O-сценариях эффект часто слабый или отрицательный.
Optional: честный контракт про отсутствие значения
Optional<T> означает: значение либо есть, либо его нет.
Optional<String> email = findEmailByUserId(10L);
String value = email.orElse("[email protected]");
Вместо неявного null вы явно заставляете вызывающий код обработать оба сценария.
Базовые операции Optional
isPresent,isEmpty;orElse,orElseGet,orElseThrow;map,flatMap,filter;ifPresent,ifPresentOrElse.
Пример 4. Цепочка без null-веток
Optional<User> user = findUser(id);
String city = user
.map(User::getAddress)
.map(Address::getCity)
.filter(c -> !c.isBlank())
.orElse("Unknown");
orElse vs orElseGet
String a = opt.orElse(expensiveFallback()); // fallback вычислится всегда
String b = opt.orElseGet(() -> expensiveFallback()); // fallback ленивый
Если fallback дорогой, используйте orElseGet.
Где Optional хорош, а где нет
Хорошо:
- Возвращаемое значение сервиса/репозитория, которое может отсутствовать.
- Границы API, где важно явно показать риск отсутствия.
Плохо:
- Поля JPA-сущностей и DTO (часто лишняя сложность).
- Аргументы методов (обычно проще перегрузка/другая сигнатура).
Частые ошибки
- Длинные stream-цепочки с тяжелой логикой внутри лямбд.
- Побочные эффекты в
map/filter. - Использование
Optional.get()без проверки. - Применение
parallelStream()«по умолчанию» без замеров.
Практический алгоритм
- Сначала сформулируйте шаги преобразования данных.
- Если шаги линейные, собирайте stream-пайплайн.
- Если логика сложная и ветвистая, не бойтесь обычного цикла.
- Для отсутствующих значений возвращайте
Optionalна границах API. - Проверяйте читаемость: код должен объяснять бизнес-смысл, а не только «работать».
Что важно запомнить
- Stream API делает обработку данных декларативной и композиционной.
- Optional делает отсутствие значения явной частью контракта.
- Оба инструмента полезны, когда их применять осознанно и без перегиба.