Optional: Кот Шрёдингера в Java 8
Представим, что в коробке находятся кот, радиоактивное вещество и колба с синильной кислотой. Вещества так мало, что в течение часа может распасться только один атом. Если в течение часа он распадётся, считыватель разрядится, сработает реле, которое приведёт в действие молоток, который разобьёт колбу, и коту настанет карачун. Поскольку атом может распасться, а может и не распасться, мы не знаем, жив ли кот или уже нет, поэтому он одновременно и жив, и мёртв. Таков мысленный эксперимент, именуемый «Кот Шрёдингера».
Класс 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 году просто обязательно.