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 emptyopt.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 exceptionorElse(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>; цепочка возвращает первый непустой. В отличие от orElse — or оставляет результат обёрнутым; 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.
Что стоит вынести из результата выполнения:
- Три конструктора
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-коде поле было бы возможно-nullAddressс геттеромOptional<Address> address(), выполняющим обёртывание.
Что дальше
Часть 12 охватила функциональный словарь от начала до конца: функциональные интерфейсы, лямбды, ссылки на методы, встроенные инструменты, стрим-пайплайн, все источники, промежуточные и терминальные операции, коллекторы, параллельное выполнение и наконец Optional как выражение отсутствия на уровне типов. Следующая глава, Java Predicate Interface, снова фокусируется на одном функциональном интерфейсе — Predicate<T> — и комбинаторной алгебре (and, or, negate, isEqual, not), позволяющей собирать предикаты без написания булевой логики вручную. Затем часть продолжается с Function, Consumer/Supplier и семейством бинарных операторов — по одному интерфейсу на главу, каждая в той же форме рабочего примера, которую вы видели здесь.