Optional

Определение

Optional<T> — контейнер, в котором есть одно ненулевое значение либо нет значения. Он делает отсутствие частью возвращаемого типа:

Optional<User> findById(long id) {
    return Optional.ofNullable(storage.get(id));
}

Это особенно удобно для поиска, где «не найдено» — нормальный исход, а не исключительная ошибка.

Создание

Optional<String> empty = Optional.empty();
Optional<String> present = Optional.of("ready");
Optional<String> maybe = Optional.ofNullable(nullableValue);

Optional.of(null) сразу выбрасывает NullPointerException. Сам Optional тоже не должен быть null: это разрушает двойственный контракт.

Преобразование

String email = repository.findById(id)
        .filter(User::active)
        .map(User::email)
        .map(String::trim)
        .orElse("unknown");
  • filter оставляет значение при выполнении условия;
  • map преобразует значение и превращает null-результат mapper в empty;
  • flatMap нужен, когда функция уже возвращает Optional, чтобы не получить вложенный контейнер.
Optional<Address> address = findUser(id)
        .flatMap(User::primaryAddress);

Получение значения

User cachedOrLoaded = cache.find(id)
        .orElseGet(() -> repository.load(id));
  • orElse(value) вычисляет аргумент до вызова, даже если значение есть;
  • orElseGet(supplier) вычисляет запасной вариант лениво;
  • orElseThrow() выбрасывает NoSuchElementException;
  • orElseThrow(factory) создаёт доменную ошибку;
  • ifPresent, ifPresentOrElse выполняют ветви как эффекты.

Прямой get() без доказанного presence обычно возвращает код к ручным проверкам и исключениям.

Optional в API

Рабочее правило: Optional полезен как возвращаемый тип, когда отсутствие ожидаемо. Для параметра обычно яснее перегрузка, отдельный тип команды или явный nullable-контракт.

Для полей entity/DTO/сериализуемых моделей Optional часто неудобен:

  • сам Optional не реализует Serializable;
  • ORM и serializers могут иметь специальные или несовместимые правила;
  • контейнер добавляет состояние «поле Optional равно null», которого быть не должно.

Внутри одной реализации nullable-переменная иногда проще. Optional ценен на границе контракта, а не как обязательная оболочка каждого значения.

Primitive Optional

OptionalInt, OptionalLong, OptionalDouble избегают boxing:

OptionalInt max = values.stream()
        .mapToInt(Item::priority)
        .max();

У них меньше combinator-методов, поэтому выбор зависит от читаемости и объёма данных.

Исключение или empty

  • empty — допустимое отсутствие (findById ничего не нашёл);
  • исключение — нарушение контракта, недоступность ресурса или другой ошибочный исход.

Нельзя превращать любую ошибку БД в Optional.empty: тогда «не найдено» становится неотличимо от сбоя.

Ловушки

  • Возвращать null вместо Optional.empty.
  • Вызывать get() без проверки.
  • Использовать orElse(expensiveCall()) вместо orElseGet.
  • Делать Optional<Optional<T>> вместо flatMap.
  • Прятать инфраструктурную ошибку как empty.
  • Использовать Optional в каждом поле и параметре без причины.

Вопросы на собеседовании

  1. Чем of отличается от ofNullable?
  2. В чём разница между map и flatMap?
  3. Почему orElse может выполнить лишнюю работу?
  4. Где Optional уместен в публичном API?
  5. Нужно ли возвращать empty при ошибке БД?
  6. Для чего существуют OptionalInt, OptionalLong, OptionalDouble?

Связанные заметки

Функциональные интерфейсы · ФИ стандартной библиотеки Java · Stream API · Работа с исключениями в Java · Классы обертки Java