W3docs

Java Optional

Выражайте отсутствие значения в Java с помощью Optional и избегайте NullPointerException по дизайну.

Optional<T> — это контейнер, который хранит либо значение типа T, либо ничего — и сообщает об этом на уровне типов, чтобы компилятор мог обязать вас обработать отсутствующий случай. Он был добавлен в Java 8 вместе со стримами, и оба спроектированы так, чтобы работать вместе: findFirst, findAny, min, max, reduce — все возвращают Optional<T> именно потому, что ответ может отсутствовать, а API предоставляет гибкие способы продолжить вычисления без единой конструкции if (x != null).

Optional — не универсальная замена null, и JDK имеет чёткое мнение о том, где его использовать. Эта глава подробно разбирает API, а затем рассматривает три ситуации, в которых Optional — неверный выбор.

Создание Optional

Три конструктора, каждый со своим точным смыслом:

Optional<String> a = Optional.of("hello");           // present; null arg throws NPE
Optional<String> b = Optional.empty();                // absent
Optional<String> c = Optional.ofNullable(maybeNull);  // present if non-null, else empty

Разница важна. Optional.of(x) — это утверждение «это значение точно здесь» — если передать null, немедленно выбрасывается NullPointerException, что именно то, что нужно (ошибка обнаруживается у источника, а не тремя кадрами ниже по стеку). Optional.ofNullable(x) — это адаптер, которым оборачивают устаревший API, возвращающий null для «отсутствует».

Внутри стрим-пайплайна вы почти никогда не создаёте Optional вручную — терминальные операции вроде findFirst и Collectors.maxBy создают их за вас.

Проверка наличия значения

Два запроса:

Optional<String> opt = lookup(id);
boolean has = opt.isPresent();      // true if a value is held
boolean none = opt.isEmpty();        // Java 11+ -- the opposite of isPresent

Вы встретите их в production-коде, но обычно это признак неудачного стиля: большинство кода, вызывающего isPresent, а затем get, было бы читабельнее с одним из операционных методов ниже. Методы запроса нужны в граничном коде, где вам действительно нужен boolean — защитное условие, выбор маршрута, ветка с предупреждением в лог.

Безопасное чтение значения

Неправильный способ:

String name = opt.get();   // throws NoSuchElementException if empty

opt.get() — это непроверяемое чтение. Именно так Optional превращается обратно в значение и исключение времени выполнения — то, от чего этот тип должен был защищать. Используйте его только после того, как убедились, что optional присутствует (или после findFirst().orElseThrow() в пайплайне, где пустой результат — ошибка программиста, а не ожидаемый случай).

Правильные способы в порядке предпочтения:

String name1 = opt.orElse("anonymous");                          // default value
String name2 = opt.orElseGet(() -> expensiveDefault());          // lazy default
String name3 = opt.orElseThrow();                                 // NoSuchElementException
String name4 = opt.orElseThrow(() -> new MyDomainError(id));      // custom exception
  • orElse(value) — задать значение по умолчанию. Значение всегда вычисляется, даже если optional присутствует, поэтому не передавайте дорогостоящие выражения.
  • orElseGet(supplier) — задать значение по умолчанию лениво. Поставщик вызывается только когда optional пуст. Используйте это для любого значения по умолчанию, которое стоит дороже литерала.
  • orElseThrow() — выбросить NoSuchElementException при отсутствии. Форма без аргументов Java 10+ — современный аналог opt.get(), когда «это точно должно присутствовать» — единственная разумная интерпретация на месте вызова.
  • orElseThrow(supplier) — выбросить доменное исключение. Стандартный способ перевести «отсутствует» в «404 not found».

Трансформация значения — map

Если optional присутствует, применяет функцию; иначе остаётся пустым:

Optional<String> upper = opt.map(String::toUpperCase);
Optional<Integer> len   = opt.map(String::length);

Сигнатура: Optional<T>.map(Function<T, R>) -> Optional<R>. Функция выполняется только при наличии значения — никаких проверок на null, никаких if и else. Это операция, ради которой Optional стоит своего числа символов: большинство цепочек «если не null, сделай это; если не null, потом сделай это» сворачиваются в .map(...).map(...).map(...).

Есть особый случай, который JDK обрабатывает тихо: если ваша функция в map возвращает null (потому что обёртывает устаревший API, возвращающий null для «нет результата»), полученный Optional будет empty(), а не Optional.of(null).

Композиция optional — flatMap

Когда функция преобразования сама возвращает Optional, map создал бы Optional<Optional<T>>. flatMap уплощает это:

record User(String id, Optional<Address> address) {}
record Address(String city) {}

Optional<String> city = userById(id)
    .flatMap(User::address)        // Optional<Address>
    .map(Address::city);            // Optional<String>

flatMap — операция, позволяющая связать несколько поисков, каждый из которых может завершиться неудачей, в единый пайплайн. Оба случая отсутствия сворачиваются в Optional.empty() в конце, и потребитель обрабатывает их один раз с orElse / orElseThrow.

Фильтрация — filter

Проверяет значение с помощью Predicate<T>; возвращает тот же optional, если проверка пройдена, или empty(), если нет:

Optional<String> nonBlank = opt.filter(s -> !s.isBlank());
Optional<Integer> positive = numberOpt.filter(n -> n > 0);

Действует как охранник внутри optional-пайплайна. Полезно, когда вопрос звучит так: «У меня есть значение, но это правильное значение для продолжения?»

Побочные эффекты — ifPresent, ifPresentOrElse

Выполнить код только при наличии значения:

opt.ifPresent(name -> log.info("hello, {}", name));

Или выполнить одну ветку при наличии и другую при отсутствии (Java 9+):

opt.ifPresentOrElse(
    name -> log.info("hello, {}", name),
    () -> log.warn("no name on the request"));

Это правильный способ выразить «сделать что-то мимоходом». Они полностью заменяют паттерн if (opt.isPresent()) { use(opt.get()); }.

Связь со стримами — Optional.stream()

(Java 9+) Преобразует Optional<T> в Stream<T> из нуля или одного элемента:

Stream<String> s = opt.stream();

Полезно внутри flatMap на Stream<Optional<T>>:

List<String> presentCities = userIds.stream()
    .map(this::userById)           // Stream<Optional<User>>
    .flatMap(Optional::stream)      // Stream<User>     -- empties drop, presents pass through
    .map(User::city)
    .toList();

Это заменяет filter(Optional::isPresent).map(Optional::get) одним flatMap(Optional::stream). Тот же результат, более чистый пайплайн.

or — откат к другому Optional

(Java 9+) При отсутствии использовать поставщик другого Optional:

Optional<User> u = primaryLookup(id)
    .or(() -> fallbackLookup(id))
    .or(() -> Optional.of(User.anonymous()));

Читается как «сначала основной поиск; если отсутствует — резервный; если отсутствует — анонимный пользователь». Все три — Optional<User>; цепочка возвращает первый непустой. В отличие от orElseor оставляет результат обёрнутым; orElse разворачивает его до обычного значения T.

Примитивные специализации

Существуют OptionalInt, OptionalLong, OptionalDouble для примитивных результатов — например, то, что возвращает IntStream.max():

OptionalInt max = nums.stream().mapToInt(Integer::intValue).max();
int hi = max.orElse(0);

У них более узкий API — нет map/flatMap/filter — потому что они находятся на границе примитивного мира. Используйте их для чтения результатов примитивных стримов; конвертируйте в Optional<Integer>, если нужен полный API.

Где Optional не принадлежит

Замысел JDK узок: Optional — это возвращаемый тип для методов, ответ которых может отсутствовать. Он не предназначен:

  • Как тип поля. Не пишите private Optional<String> middleName;. Он не реализует Serializable, требует выделения памяти на каждое поле, а поле null короче и понятнее для «у сущности нет отчества». Правильный подход — поле без Optional, которое может быть null, с геттером, возвращающим Optional.
  • Как параметр метода. Не принимайте Optional<String> в качестве аргумента. Перегрузите метод или принимайте String, документируя, что null означает отсутствие. Optional-параметры требуют от вызывающего кода обёртывания, что является лишним шумом.
  • Как элемент коллекции. List<Optional<T>> — почти всегда список с null-допустимыми элементами и лишней обёрткой. Используйте List<T> и фильтруйте null-значения на границе, или используйте flatMap(Optional::stream) для удаления отсутствующих в пайплайне.
  • Как способ избежать всех null. В Java по-прежнему есть null в каждом ссылочном типе; Optional нужен для формы возврата кода, производящего значения, которые могут отсутствовать. Обычные ссылочные типы подходят для всего остального.

Краткое правило: Optional, вытекающий из метода — хороший дизайн; Optional, втекающий в метод — почти всегда ошибка.

Рабочий пример: все методы и практические правила в коде

Программа ниже строит небольшой граф пользователей и адресов, проходит все методы Optional, демонстрирует разницу в времени вычисления orElse и orElseGet, мост Optional.stream() и цепочку or.

java— editable, runs on the server

Что стоит вынести из результата выполнения:

  • Три конструктора of, empty, ofNullable соответствуют трём чётким намерениям: точно присутствует, точно отсутствует и адаптер для устаревшего кода, присутствует если не null. Optional.of(null) выбрасывает исключение — и это желаемый сбой, а не ошибка, которую нужно обходить.
  • orElse вычислял свой аргумент каждый раз, даже когда optional присутствовал. Поставщик orElseGet срабатывал только при необходимости. Используйте orElse для дешёвых литералов и orElseGet для всего, что выделяет память, делает запросы или может выбросить исключение.
  • map и flatMap позволили всей цепочке userById(...).flatMap(User::address).map(Address::city) читаться как единый пайплайн — никаких проверок на null, никаких вложенных условий, и любой пустой шаг замыкает цепь на Optional.empty() в конце.
  • flatMap(Optional::stream) преобразовал Stream<Optional<User>> в Stream<User> с отброшенными отсутствующими значениями за один шаг. Это чистый способ преобразовать список «потенциально неудачных» поисков в стрим успешных результатов.
  • OptionalInt — это то, что возвращают терминалы примитивных стримов, такие как IntStream.findFirst. У него есть собственный небольшой API (getAsInt, orElse, ifPresent), и он существует, чтобы примитивные пайплайны никогда не выполняли боксинг.
  • Правило «неправильных мест» проявилось неявно: User.address был полем Optional<Address> — допустимо, потому что пример хотел продемонстрировать API, но в production-коде поле было бы возможно-null Address с геттером Optional<Address> address(), выполняющим обёртывание.

Что дальше

Часть 12 охватила функциональный словарь от начала до конца: функциональные интерфейсы, лямбды, ссылки на методы, встроенные инструменты, стрим-пайплайн, все источники, промежуточные и терминальные операции, коллекторы, параллельное выполнение и наконец Optional как выражение отсутствия на уровне типов. Следующая глава, Java Predicate Interface, снова фокусируется на одном функциональном интерфейсе — Predicate<T> — и комбинаторной алгебре (and, or, negate, isEqual, not), позволяющей собирать предикаты без написания булевой логики вручную. Затем часть продолжается с Function, Consumer/Supplier и семейством бинарных операторов — по одному интерфейсу на главу, каждая в той же форме рабочего примера, которую вы видели здесь.

Практика

Практика
У вас есть `Optional<String> opt` и нужно значение по умолчанию при его отсутствии, где значение по умолчанию — дорогой вызов `loadDefaultFromDb()`. Что правильно *и* избегает выполнения дорогого вызова, когда `opt` присутствует?
У вас есть `Optional<String> opt` и нужно значение по умолчанию при его отсутствии, где значение по умолчанию — дорогой вызов `loadDefaultFromDb()`. Что правильно *и* избегает выполнения дорогого вызова, когда `opt` присутствует?
Was this page helpful?