Логотип Workflow

Article

Stream Api And Optional

Тема 10. Stream API, Optional

Stream API и Optional решают две повседневные задачи:

  1. Понятно описывать обработку коллекций как конвейер шагов.
  2. Явно работать со сценарием «значение может отсутствовать».

Stream API: идея и модель

Stream это не коллекция, а последовательность вычислений над данными.

Ключевые свойства:

  • не хранит элементы сам по себе;
  • обычно не изменяет исходную коллекцию;
  • промежуточные операции ленивые;
  • запуск происходит на терминальной операции.

Stream pipeline

Из чего состоит stream-конвейер

  1. Source (источник): list.stream(), Stream.of(...), Files.lines(...).
  2. Intermediate operations: filter, map, flatMap, sorted, distinct, limit.
  3. 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 уместен, а когда лучше цикл

Уместен:

  1. Трансформация данных в несколько шагов.
  2. Фильтрация + агрегация.
  3. Декларативный код без сложных побочных эффектов.

Лучше цикл:

  1. Сложная ветвистая бизнес-логика с состоянием.
  2. Нужна тонкая отладка на каждом шаге.
  3. Критичный 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 хорош, а где нет

Хорошо:

  1. Возвращаемое значение сервиса/репозитория, которое может отсутствовать.
  2. Границы API, где важно явно показать риск отсутствия.

Плохо:

  1. Поля JPA-сущностей и DTO (часто лишняя сложность).
  2. Аргументы методов (обычно проще перегрузка/другая сигнатура).

Частые ошибки

  1. Длинные stream-цепочки с тяжелой логикой внутри лямбд.
  2. Побочные эффекты в map/filter.
  3. Использование Optional.get() без проверки.
  4. Применение parallelStream() «по умолчанию» без замеров.

Практический алгоритм

  1. Сначала сформулируйте шаги преобразования данных.
  2. Если шаги линейные, собирайте stream-пайплайн.
  3. Если логика сложная и ветвистая, не бойтесь обычного цикла.
  4. Для отсутствующих значений возвращайте Optional на границах API.
  5. Проверяйте читаемость: код должен объяснять бизнес-смысл, а не только «работать».

Что важно запомнить

  • Stream API делает обработку данных декларативной и композиционной.
  • Optional делает отсутствие значения явной частью контракта.
  • Оба инструмента полезны, когда их применять осознанно и без перегиба.

Quiz

Проверьте, что вы усвоили

Авторизуйтесь чтоб пройти тесты