Введение в Optional

В JDK 8 был введён новый утилитарный класс java.util.Optional, целями которого являются повышение null-safety и читаемости кода при работе со ссылками, значения которых могут быть null. Главная же цель Optional — замена null-значений, благодаря чему должна повышаться безопасность и читаемость кода.

Optional тесно связан с другими нововведениями JDK 8: Stream и лямбда-выражениями.

Пример использования

Допустим, у нас есть класс Employee, интерфейс EmployeeRepository для работы с источником данных и его реализация CachingEmployeeRepository, которая реализует кэширование и делегирует вызовы объекту другого реализующего класса.

В методе findOneById, идеальном для использования кэширования, общая последовательность действий выглядит следующим образом:

  • Попытаться получить объект Employee из кэша
  • Если кэш вернул null, то вызвать метод findOneById у объекта-делегата
  • Если делегат вернул не null, то сохранить значение в кэше
  • Вернуть значение или выкинуть исключение NoSuchElementException

Примерно так будет выглядеть реализация метода findOneById без использования Optional:

А так будет выглядеть реализация этого же метода уже с использованием Optional:

Обратите внимание, что в коде отсутствуют проверки на null, что достигнуто благодаря Optional. Но тут есть два неудобных момента:

  • Если предполагается несколько вариантов получения объекта, то цепочка действий превратится эдакую «лесенку»: мы создаём Optional, в методе orElseGet создаём альтернативный Optional и т.д.
  • Отсутствует метод, который бы принимал в качестве аргумента объект класса java.util.function.Consumer для работы со значением Optional, если оно представлено, аналогично Stream.peek из JDK или Mono.doOnNext из Reactor.

Первый неудобный момент решается использованием метода or вместо orElseGet, который был добавлен в JDK 9. С его применением, метод findOneById приобретёт следующий вид:

Пример OTN

В оригинальной статье на OTN, посвящённой Optional, в качестве примера приводилась следующая структура классов:

В результате чего абстрактная попытка получить версию USB установленной в компьютере звуковой карты будет выглядеть следующим образом:

Первый вариант может привести к NullPointerException, второй же исключает эту ситуацию, но требует большого количества кода для реализации достаточно простой задачи. Вариант с применением Optional будет выглядеть значительно проще:

Работа с Optional

Теперь, когда мы разобрали пару примеров использования Optional, можно разобраться с методами этого класса.

Методы создания

Объект типа Optional может быть создан тремя методами:

  • Optional.of — для создания Optional из не-null значения. При передаче в качестве аргумента null будет выкинуто исключение NullPointerException.
  • Optional.ofNullable — для создания Optional из значения, которое может быть null. Если значение не null, то будет создан Optional со значением, иначе — пустой Optional.
  • Optional.empty — для создания пустого Optional.

Методы преобразования

Непустой Optional может быть преобразован в другой Optional, который может содержать другое значение, значение другого типа или и вовсе быть пустым. Поскольку методы filter, map, flatMap и orвозвращают объекты типа Optional, можно создавать цепочки преобразований практически любой длины.

filter() — фильтрация значения

Методом filter значение Optional может быть «отфильтровано». В качестве аргумента метод принимает объект типа java.util.function.Predicate. Если значение удовлетворяет условиям фильтрации, то возвращён будет тот же Optional, в противном случае — пустой Optional.

map() и flatMap() — преобразование значения

Методом map из текущего Optional можно получить новый объект Optional, содержащий другое значение. В качестве аргумента метод принимает объект типа java.util.function.Function. Если переданная функция возвращает null, то метод map вернёт пустой Optional.

Метод flatMap работает аналогично, но он декларирует, что переданная функция должна возвращать другой Optional, пустой или содержащий новое значение.

or()

В JDK 9 был добавлен метод or, позволяющий пустой Optional преобразовать в непустой. В качестве аргумента он принимает объект типа java.util.function.Supplier, метод get которого возвращает новый Optional и будет вызван только в том случае, если существующий Optional пустой.

Методы проверки

Единственным до JDK 11 методом проверки наличия значения был isPresent, который возвращает true, если объект Optional содержит значение, иначе — false.

В JDK 11 был добавлен метод-антипод isEmpty, возвращающий true, если значение отсутствует.

Методы получения значения

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

get()

Метод get — простейший способ получения значения объекта Optional. Если Optional непустой, то он вернёт значение, иначе выбросит исключение NoSuchElementException. Поэтому при обращении к этому методу нужно либо проверять наличие значения при помощи Optional.isPresent(), либо использовать try/catch.

orElse()

Метод orElse возвращает значение объекта Optional или значение, переданное ему в качестве аргумента, которое может быть null.

orElseGet()

Метод orElseGet, в отличии от orElse принимает в качестве аргумента объект класса java.util.function.Supplier, который вернёт «запасное» значение, если объект Optional пустой. Таким образом, в отличии от метода orElse, метод orElseGet создаёт «запасное» значение только по необходимости, что немного правильнее с точки зрения использования ресурсов, особенно, когда получение значения умолчанию требует выполнения каких-либо блокирующих операций, например, обращения к базе данных.

orElseThrow()

Метод orElseThrow возвращает значение объекта Optional или выбрасывает исключение, возвращаемое объектом типа java.util.function.Supplier переданным ему в качестве аргумента.
В JDK 10 был добавлен orElseThrow без аргументов, выбрасывающий NoSuchElementException.

Условные методы

Условные методы ifPresent и ifPresentOrElse позволяют выполнить необходимое действие над значением объекта Optional, если оно задано.

ifPresent()

Метод ifPresent принимает в качестве аргумента объект типа java.util.function.Consumer, метод accept которого принимает текущее значение объекта Optional в качестве аргумента. Метод accept будет вызван только в том случае, если Optional содержит значение.

ifPresentOrElse()

Метод ifPresentOrElse аналогичен методу ifPresent, он был добавлен в JDK 9 и в качестве второго аргумента принимает объект типа java.lang.Runnable, метод run которого будет вызван, если объект Optional не содержит значения.

Прочие методы

Optional реализует методы hashCode, equals и toString с использованием хранимого значения. Вызовы hashCode и equals делегируются объектам, использующимся в качестве значений Optional, а toString составляет строчное представление с использованием строчного представление объекта-значения.

Добавленный в JDK 9 метод stream возвращает объект типа java.util.stream.Stream, содержащий либо один элемент, либо пустой.

Заключение

Как бы то ни было, Optional не решает проблему NullPointerException полностью, но при правильном применении значительно снижает шансы возникновения этого исключения. При использовании Optional совместно с лямбда-выражениями код может выглядеть не совсем очевидно, что особенно хорошо видно по примеру с цепочкой из нескольких вызовов Optional.orElseGet(), но одновременно с этим код с использованием Optional в большинстве случаев будет выглядеть более логичным, чем с несколькими вложенными блоками if/else.