Optional: Кот Шрёдингера в Java 8

Представим, что в коробке находятся кот, радиоактивное вещество и колба с синильной кислотой. Вещества так мало, что в течение часа может распасться только один атом. Если в течение часа он распадётся, считыватель разрядится, сработает реле, которое приведёт в действие молоток, который разобьёт колбу, и коту настанет карачун. Поскольку атом может распасться, а может и не распасться, мы не знаем, жив ли кот или уже нет, поэтому он одновременно и жив, и мёртв. Таков мысленный эксперимент, именуемый «Кот Шрёдингера».

jqxe-aixkqqrnxpwvwbm4huavh4.jpeg

Класс Optional обладает схожими свойствами — при написании кода разработчик часто не может знать — будет ли существовать нужный класс на момент исполнения программы или нет, и в таких случаях приходится делать проверки на null. Если такими проверками пренебречь, то рано или поздно (обычно рано) Ваша программа рухнет с NullPointerException.

Коллеги! Статья, как и любая другая, не идеальна и может быть поправлена. Если Вы видите возможность существенного улучшения данного материала, укажите её в комментариях.

Как получить объект через Optional?


Как уже было сказано, класс Optional может содержать объект, а может содержать null. К примеру, попытаемся извлечь из репозитория юзера с заданным ID:

User = repository.findById(userId);


Возможно, юзер по такому ID есть в репозитории, а возможно, нет. Если такого юзера нет, к нам в стектрейс прилетает NullPointerException. Не имей мы в запасе класса Optional, нам пришлось бы изобретать какую-нибудь такую конструкцию:

User user;
if (Objects.nonNull(user =  repository.findById(userId))) {
(остальная борода пишется тут)
}


Согласитесь, не очень. Намного приятнее иметь дело с такой строчкой:

Optional user = Optional.of(repository.findById(userId));


Мы получаем объект, в котором может быть запрашиваемый объект —, а может быть null. Но с Optional надо как-то работать дальше, нам нужна сущность, которую он содержит (или не содержит).

Cуществует всего три категории Optional:

  • Optional.of — возвращает Optional-объект.


  • Optional.ofNullable -возвращает Optional-объект, а если нет дженерик-объекта, возвращает пустой Optional-объект.


  • Optional.empty — возвращает пустой Optional-объект.


Существует так же два метода, вытекающие из познания, существует обёрнутый объект или нет — isPresent () и ifPresent ();

.ifPresent ()


Метод позволяет выполнить какое-то действие, если объект не пустой.

Optional.of(repository.findById(userId)).ifPresent(createLog());


Если обычно мы выполняем какое-то действие в том случае, когда объект отсутствует (об этом ниже), то здесь как раз наоборот.

.isPresent ()


Этот метод возвращает ответ, существует ли искомый объект или нет, в виде Boolean:

Boolean present = repository.findById(userId).isPresent();


Если Вы решили использовать нижеописанный метод get (), то не будет лишним проверить, существует ли данный объект, при помощи этого метода, например:

Optional optionalUser = repository.findById(userId);
User user = optionalUser.isPresent() ? optionalUser.get() : new User();


Но такая конструкция лично мне кажется громоздкой, и о более удобных методах получения объекта мы поговорим ниже.

Как получить объект, содержащийся в Optional?


Существует три прямых метода дальнейшего получения объекта семейства orElse (); Как следует из перевода, эти методы срабатывают в том случае, если объекта в полученном Optional не нашлось.

  • orElse () — возвращает объект по дефолту.


  • orElseGet () — вызывает указанный метод.


  • orElseThrow () — выбрасывает исключение.


.orElse ()


Подходит для случаев, когда нам обязательно нужно получить объект, пусть даже и пустой. Код, в таком случае, может выглядеть так:

User user = repository.findById(userId).orElse(new User());


Эта конструкция гарантированно вернёт нам объект класса User. Она очень выручает на начальных этапах познания Optional, а также, во многих случаях, связанных с использованием Spring Data JPA (там большинство классов семейства find возвращает именно Optional).

.orElseThrow ()


Очень часто, и опять же, в случае с использованием Spring Data JPA, нам требуется явно заявить, что такого объекта нет, например, когда речь идёт о сущности в репозитории. В таком случае, мы можем получить объект или, если его нет, выбросить исключение:

User user = repository.findById(userId).orElseThrow(() -> new NoEntityException(userId));


Если сущность не обнаружена и объект null, будет выброшено исключение NoEntityException (в моём случае, кастомное). В моём случае, на клиент уходит строчка «Пользователь {userID} не найден. Проверьте данные запроса».

.orElseGet ()


Если объект не найден, Optional оставляет пространство для «Варианта Б» — Вы можете выполнить другой метод, например:

User user = repository.findById(userId).orElseGet(() -> findInAnotherPlace(userId));


Если объект не был найден, предлагается поискать в другом месте.

Этот метод, как и orElseThrow (), использует Supplier. Также, через этот метод можно, опять же, вызвать объект по умолчанию, как и в .orElse ():

User user = repository.findById(userId).orElseGet(() -> new User());


Помимо методов получения объектов, существует богатый инструментарий преобразования объекта, морально унаследованный от stream ().

Работа с полученным объектом.


Как я писал выше, у Optional имеется неплохой инструментарий преобразования полученного объекта, а именно:

  • get () — возвращает объект, если он есть.


  • map () — преобразовывает объект в другой объект.


  • filter () — фильтрует содержащиеся объекты по предикату.


  • flatmap () — возвращает множество в виде стрима.


.get ()


Метод get () возвращает объект, запакованный в Optional. Например:

User user = repository.findById(userId).get();


Будет получен объект User, запакованный в Optional. Такая конструкция крайне опасна, поскольку минует проверку на null и лишает смысла само использование Optional, поскольку Вы можете получить желаемый объект, а можете получить NPE. Такую конструкцию придётся оборачивать в .isPresent ().

.map ()


Этот метод полностью повторяет аналогичный метод для stream (), но срабатывает только в том случае, если в Optional есть не-нулловый объект.

String name = repository.findById(userId).map(user -> user.getName()).orElseThrow(() -> new Exception());


В примере мы получили одно из полей класса User, упакованного в Optional.

.filter ()


Данный метод также позаимствован из stream () и фильтрует элементы по условию.

List users = repository.findAll().filter(user -> user.age >= 18).orElseThrow(() -> new Exception());


.flatMap ()


Этот метод делает ровно то же, что и стримовский, с той лишь разницей, что он работает только в том случае, если значение не null.

Заключение


Класс Optional, при умелом использовании, значительно сокращает возможности приложения рухнуть с NullPoinerException, делая его более понятным и компактным, чем как если бы Вы делали бесчисленные проверки на null. А если Вы пользуетесь популярными фреймворками, то Вам тем более придётся углублённо изучить этот класс, поскольку тот же Spring гоняет его в своих методах и в хвост, и в гриву. Впрочем, Optional — приобретение Java 8, а это значит, что знать его в 2018 году просто обязательно.

© Habrahabr.ru