🌊 Розділ 8 · Питання #29

Як працювати з Optional в Stream

Optional та Stream спроектовані для спільної роботи у функціональному стилі. Основні сценарії взаємодії:


🟢 Junior Level

Коротка відповідь для співбесіди (30 секунд)

Optional<T> та Stream<T> спроектовані для спільної роботи у функціональному стилі. Основні сценарії взаємодії:

  1. Термінальні операції Stream повертають Optional: операції findFirst(), findAny(), min(), max(), reduce(accumulator) повертають Optional<T>, оскільки результуючий потік може бути порожнім.
  2. Розпакування Stream<Optional<T>> у плаский Stream<T>: починаючи з Java 9, для безпечного виключення порожніх контейнерів використовується метод .flatMap(Optional::stream).
  3. Безпечне вилучення значень: категорично заборонено викликати .get() без попередньої перевірки. Натомість використовуються методи orElse(defaultValue), orElseGet(Supplier) або orElseThrow().

Приклади використання на Java 21

import java.util.List;
import java.util.Optional;

public class OptionalStreamDemo {

    public record User(int id, String name, String email) {}

    public static void main(String[] args) {
        List<Optional<String>> rawEmails = List.of(
            Optional.of("user1@example.com"),
            Optional.empty(),
            Optional.of("user3@example.com")
        );

        // 1. Перетворення Stream<Optional<String>> -> Stream<String> (Java 9+)
        List<String> validEmails = rawEmails.stream()
            .flatMap(Optional::stream) // Порожні Optional автоматично відсіваються!
            .map(String::toLowerCase)
            .toList();

        System.out.println("Валідні email: " + validEmails); // [user1@example.com, user3@example.com]

        // 2. Пошук із безпечним вилученням через orElse
        List<User> users = List.of(new User(1, "Іван", "ivan@mail.ua"));

        String foundName = users.stream()
            .filter(u -> u.id() == 2)
            .findFirst()
            .map(User::name)
            .orElse("Гість");

        System.out.println("Користувач: " + foundName); // Гість
    }
}

🟡 Middle Level

Еволюція розпакування: Java 8 проти Java 9+

Java 8 стиль (двоетапна фільтрація):

List<String> listJava8 = rawOptionals.stream()
    .filter(Optional::isPresent)
    .map(Optional::get) // Виглядає багатослівно, потенційний ризик NPE при помилці
    .toList();

Java 9+ стиль (Optional.stream()):

Метод Optional.stream() перетворює екземпляр Optional:

  • якщо значення присутнє $\rightarrow$ Stream.of(value) (потік з 1 елемента);
  • якщо порожньо $\rightarrow$ Stream.empty() (порожній потік).

При виклику .flatMap(Optional::stream) усі одиничні потоки об’єднуються, а порожні підпотоки безслідно зникають:

List<String> listJava9 = rawOptionals.stream()
    .flatMap(Optional::stream) // Лаконічно та безпечно
    .toList();

Різниця: orElse() проти orElseGet() всередині стрімів

Часта архітектурна помилка — обчислення дефолтних значень:

// ❌ НЕБЕЗПЕЧНО: orElse() ЗАВЖДИ виконує аргумент, навіть якщо значення присутнє!
String result = opt.orElse(callHeavyRemoteService());

// ✅ ПРАВИЛЬНО: orElseGet() виконує лямбду ТІЛЬКИ якщо Optional порожній (ліниво)!
String safeResult = opt.orElseGet(() -> callHeavyRemoteService());

Всередині конвеєра стрімів передача важких методів (запитів до БД, генерації токенів) в orElse() призводить до їхнього холостого виконання для кожного елемента потоку.


🔴 Senior Level

Архітектурні правила використання Optional в Enterprise

  1. Optional — лише для значень, що повертаються: Творець Optional Браян Ґец (Brian Goetz) прямо наголошував: клас Optional створювався як тип значення, що повертається для бібліотечних методів, де відсутність результату є очікуваним нормальним сценарієм (findFirst(), findById()).
  2. Антипатерн: Optional у полях Entity та DTO:
    • Клас java.util.Optional не реалізує інтерфейс java.io.Serializable.
    • Розміщення Optional у полях JPA/Hibernate сутностей призводить до збоїв серіалізації в розподілених кешах (Redis, Hazelcast) та сесіях.
    • Збільшує footprint: кожен Optional — це окремий посилальний об’єкт у купі (16–24 байти).
  3. Антипатерн: Optional у параметрах методів: Передача Optional<T> як аргументу методу ускладнює клієнтський виклик (доводиться писати foo(Optional.of(x))) і відкриває можливість передати null замість самого Optional, що поверне проблему NPE назад.

Highload та примітивні спеціалізації

У високонавантажених системах (FinTech, рекламні аукціони RTB):

  • Створення сотень тисяч об’єктів Optional<Integer> перевантажує збирач сміття (GC pressure).
  • Для примітивних типів завжди використовуються спеціалізовані стріми та контейнери: IntStream.max() $\rightarrow$ OptionalInt LongStream.findFirst() $\rightarrow$ OptionalLong DoubleStream.average() $\rightarrow$ OptionalDouble

Вони зберігають значення у звичайному примітивному полі int/long/double та логічний прапорець isPresent, не створюючи об’єктів у купі при інлайнінгу.


Методи Java 9–11 для функціональних ланцюжків

// 1. or() (Java 9+): ланцюжок резервних пошуків
Optional<User> user = findInCache(id)
    .or(() -> findInDatabase(id))
    .or(() -> findInExternalApi(id));

// 2. ifPresentOrElse() (Java 9+): розгалуження без if-else
findUser(id).ifPresentOrElse(
    u -> sendWelcomeEmail(u),
    () -> log.warn("Користувача не знайдено")
);

// 3. isEmpty() (Java 11+): лаконічна інверсія isPresent()
if (userOpt.isEmpty()) { ... }

🎯 Шпаргалка для інтерв’ю

30-секундна відповідь

«Optional та Stream органічно доповнюють один одного. Термінальні методи стрімів (findFirst, reduce, min) повертають Optional, виключаючи повернення null. Для перетворення Stream<Optional<T>> на плаский Stream<T> у сучасному Java-коді використовується .flatMap(Optional::stream). Головне правило: Optional призначений лише для типів, що повертаються з методів; використовувати його в полях класів (особливо JPA Entity) та параметрах методів не можна. При виборі між orElse та orElseGet завжди слід віддавати перевагу лінивому orElseGet для запобігання холостим обчисленням.»


Зведення методів роботи з Optional у Stream

Задача Рекомендований підхід Чого уникати
Сплющити Stream<Optional<T>> .flatMap(Optional::stream) .filter(o -> o.isPresent()).map(o -> o.get())
Безпечний дефолт .orElseGet(this::calculateDefault) .orElse(calculateDefault()) (холості виклики)
Викидання помилки при відсутності .orElseThrow(() -> new NotFoundEx()) .get() без перевірок (NoSuchElementException)
Створення з можливого null Optional.ofNullable(val) Optional.of(val) (викине NPE при null)
Примітивні стріми OptionalInt, OptionalLong Боксинг в Optional<Integer>

Топ-4 каверзних запитання на інтерв’ю

1. Як у Java 9+ лаконічно перетворити Stream<Optional<T>> на Stream<T> без порожніх елементів?

  • Викликати метод .flatMap(Optional::stream). Метод Optional.stream() перетворює непорожній контейнер на стрім з одного елемента, а порожній — на Stream.empty(). Метод flatMap сплющує всі одиничні стріми в єдиний потік, автоматично відсіваючи порожні.

2. У чому різниця між .orElse(compute()) та .orElseGet(this::compute) всередині стріму?

  • Метод .orElse(compute()) приймає готове значення: вираз compute() обчислюватиметься завжди, навіть якщо Optional містить значення.
  • Метод .orElseGet(Supplier) приймає лінивий інтерфейс: код обчислення виконається лише в тому разі, якщо Optional дійсно порожній.

3. Чому Optional не повинен використовуватися як поле JPA Entity?

  • По-перше, клас Optional не реалізує Serializable, що призведе до помилок під час серіалізації сутності фреймворками кешування.
  • По-друге, специфікація JPA/Hibernate не підтримує зіставлення полів Optional із колонками таблиць БД безпосередньо.
  • По-третє, це створює зайві алокації пам’яті в купі для кожного екземпляра сутності.

4. У чому різниця між Optional.of() та Optional.ofNullable()?

  • Optional.of(value) вимагає суворо ненульове значення і викидає NullPointerException, якщо передано null.
  • Optional.ofNullable(value) безпечно обробляє null, повертаючи в цьому випадку порожній екземпляр Optional.empty().

Типові помилки та Red Flags

  • 🚩 Виклик методу .get() на Optional без попередньої перевірки isPresent().
  • 🚩 Використання Optional як типів полів класу або параметрів методів.
  • 🚩 Виклик важких методів усередині orElse() замість orElseGet().
  • 🚩 Спроба запакувати null через Optional.of(null), що викликає миттєвий NPE.

Пов’язані теми