Тема 10. Stream API and Optional
Stream API и Optional в Java нужны, чтобы делать код обработки данных более предсказуемым и читаемым. Stream помогает описывать преобразования как конвейер шагов, а Optional заставляет явно обработать сценарий «значения нет». Для новичка это два инструмента, которые резко уменьшают хаос с null и вложенными циклами.
1. Как мыслить Stream API
Stream — это не контейнер, а pipeline вычислений над источником данных. Источник может быть коллекцией, массивом, файлом.
Базовая модель:
source(источник),- промежуточные операции (
filter,map,flatMap...), - терминальная операция (
toList,collect,count,findFirst...).
Пример:
List<String> names = List.of("ann", "bob", "alex", "anna");
List<String> result = names.stream()
.filter(n -> n.length() > 3)
.map(String::toUpperCase)
.sorted()
.toList();
2. Ленивая природа stream
Промежуточные операции ленивые. Пока нет терминальной операции, реального прохода по данным нет.
Stream<String> s = names.stream().filter(n -> n.startsWith("a"));
// вычисления еще не выполнены
long count = s.count(); // запуск pipeline
Это помогает оптимизировать цепочки и не делать лишнюю работу.
3. Частые операции с примерами
map — преобразовать каждый элемент
List<Integer> lengths = names.stream()
.map(String::length)
.toList();
filter — оставить элементы по условию
List<String> shortNames = names.stream()
.filter(n -> n.length() <= 3)
.toList();
flatMap — «распаковать» вложенные коллекции
List<List<String>> tags = List.of(
List.of("java", "backend"),
List.of("backend", "spring")
);
List<String> uniqueTags = tags.stream()
.flatMap(List::stream)
.distinct()
.sorted()
.toList();
collect и groupingBy
Map<Integer, List<String>> byLength = names.stream()
.collect(java.util.stream.Collectors.groupingBy(String::length));
reduce для свертки
List<Integer> prices = List.of(100, 250, 80, 70);
int total = prices.stream().reduce(0, Integer::sum);
4. Когда stream лучше, а когда лучше цикл
| Ситуация | Подход |
|---|---|
| Линейная трансформация | Stream |
| Сложные ветвления и состояние | Цикл |
| Агрегации и группировки | Stream |
| Детальная пошаговая отладка | Цикл |
Прагматичное правило: выбирайте не «модный» стиль, а тот, который легче читать и поддерживать команде.
5. Optional: явный контракт «значение может отсутствовать»
Optional<T> показывает, что значение необязательно присутствует.
Optional<String> email = findEmailByUserId(10L);
String value = email.orElse("[email protected]");
Это лучше неявного возврата null, потому что вызывающий код вынужден обработать оба сценария.
6. Базовые операции Optional
orElse,orElseGet,orElseThrowmap,flatMap,filterifPresent,ifPresentOrElse
Пример цепочки без null-веток:
Optional<User> user = findUser(id);
String city = user
.map(User::getAddress)
.map(Address::getCity)
.filter(c -> !c.isBlank())
.orElse("Unknown");
7. Важный нюанс orElse vs orElseGet
String a = opt.orElse(expensiveFallback());
String b = opt.orElseGet(() -> expensiveFallback());
orElse вычисляет fallback всегда, даже если значение уже есть. orElseGet — ленивый вариант.
8. Частые ошибки новичков
- Длинные stream-цепочки с тяжелой бизнес-логикой внутри лямбд.
- Побочные эффекты внутри
map/filter. - Использование
Optional.get()без гарантии присутствия. - Применение
parallelStream()без замеров.
9. Примеры «как лучше»
Плохо:
String city = user.getAddress().getCity(); // NullPointerException risk
Лучше:
String city = Optional.ofNullable(user)
.map(User::getAddress)
.map(Address::getCity)
.orElse("Unknown");
Плохо:
list.stream().map(x -> { log(x); return transform(x); }).toList();
Лучше разделять вычисление и побочные эффекты.
10. Что важно запомнить
Stream API — про декларативную обработку данных. Optional — про честный контракт отсутствия значения. В связке они помогают писать чище, если соблюдать меру: короткие понятные цепочки, минимум побочных эффектов и осознанный выбор между stream и обычным циклом.