Класс 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> 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<User> 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<User> users = repository.findAll().filter(user -> user.age >= 18).orElseThrow(() -> new Exception());
.flatMap()
Этот метод делает ровно то же, что и стримовский, с той лишь разницей, что он работает только в том случае, если значение не null.
Заключение
Класс Optional, при умелом использовании, значительно сокращает возможности приложения рухнуть с NullPoinerException, делая его более понятным и компактным, чем как если бы Вы делали бесчисленные проверки на null. А если Вы пользуетесь популярными фреймворками, то Вам тем более придётся углублённо изучить этот класс, поскольку тот же Spring гоняет его в своих методах и в хвост, и в гриву. Впрочем, Optional — приобретение Java 8, а это значит, что знать его в 2018 году просто обязательно.
Комментарии (48)
dernasherbrezon
22.01.2018 10:38User user; if (Objects.nonNull(user = repository.findById(userId))) { (остальная борода пишется тут) }
Действительно, не очень. Зовите меня старпёром, но последние 20 лет я пишу так, и проблем не было:
User user = repository.findById(userId); if( user != null ) { (остальная борода пишется тут) }
mantiscorp
22.01.2018 12:11+1Вы — старпёр :)
На самом деле, если надо просто проверить User на null, Ваш метод прекрасно работает. Более того, он даже быстрее варианта с Optional, потому что не создаётся/уничтожается объект Optional.
Но представьте, что у User есть поле address, которое может быть null, в котором есть поле ZIP, которое тоже nullable. Вам надо отобразить это самое последнее поле. Без Optional.map() это будет жуткое количество проверок на null, а с Optional.map() — только одна финальнаяdernasherbrezon
22.01.2018 13:10+1Мне было действительно интересно насколько я старпёр. Так что я это померил (ради лулзов конечно же).
DISCLAIMER: автор не несёт ответственности за причинённый кому либо моральный ущерб
Я решил сравнить время написания кода проверок на null и время потраченное компьютером на выполнения кода с Optional.
if (address != null && address.getZip() != null) { System.out.println(address.getZip().getValue()); }
if (op.map(cur -> cur.getZip()).isPresent()) { (остальная борода пишется тут) }
Проверки почти одинаковые, и даже если немного разные, то это не влияет на суть исследования.
Секундомер на столе показал, что я могу написать условие на null за:
- 23s
- 16s
- 14s
Будем брать самый первый результат, потому что в реальной жизни приходится часто опечатываться. Даже с помощью IDE я опечатался 2 раза.
Теперь замерим сколько будет выполнять тот и другой код. Простой цикл с замером времени в миллисекундах. Кто хочет, может подключить JMH и получить титул "зануда месяца".
Обычные проверки дали:
38 ms
Проверки с Optional:
88 ms
Настало время анализа. Для этого нужен график. Например, такой:
Из него следует, что выигрыш в использовании обычных проверок на null наступает в районе 461 вызова. Т.е. если Ваш код вызывается меньше, чем 461, то выгоднее использовать Optional, если больше, то для экономии всегобщего времени человечества нужно делать проверку вручную.
mantiscorp
22.01.2018 13:20isPresent() вообще не нужно:
согласитесь, что код выглядит намного чище за счёт полного устранения проверок на null.System.out.println(user.map(User::getAddress).map(Address::getZip).orElse(""));
dernasherbrezon
22.01.2018 13:45-1Вкусовщина. Мне недавно пришел вот такой код на ревью:
Optional.ofNullable(toKill).ifPresent( p -> { do something with p. })
Автор, когда писал этот код, просто хотел сделать:
if( toKill != null ) { do something with toKill. }
Но вместо этого он заставил всех потомков и других разработчиков держать в голове целых 3 конструкции:
- Optional.ofNullable и его интерфейс
- ifPresent, которое как раз делает преобразование в is not null в is present
- переименование toKill в p.
Читаемость такого кода так себе.
xpendence Автор
22.01.2018 12:1320 лет? Пишете на Java 1.1?
Спасибо за информацию, теперь я буду знать, что Вы так пишете.
Adverte
22.01.2018 12:33я придерживаюсь стратегии ставить null в операции сравнения на первое место
if( user != null ) {}
if( null != user ) {}
поможет избежать дополнительный поиск опечатки в случае если напишете = вместо ==
if( null = user ) {}
dernasherbrezon
22.01.2018 12:37Мой, опять же старпёрский, инструмент Eclipse говорит что "cannot convert from Address to boolean". Проблем не будет если ставить null в конце.
xpendence Автор
22.01.2018 12:38Язык развивается, старые рецепты заменяются новыми. Также, если Вы хотите работать с последними версиями популярных фреймворков, Вам будет необходимо изучить Optional, а если хотите успешно с ними работать, то выучить его нужно будет хорошо.
Также, как уже было указано в статье, Optional очень хорошо чистит код и делает его более читабельным. Ведь такого рода проверки на null являются служебными и только мешают видеть суть кода.
И, кстати, вместо
if (user == null) {}
давно уже принято использовать
if (Objects.isNull(user)) {}
Azargan
22.01.2018 13:55Можно уточнить где принято так использовать?
javadoc для Objects.isNull() говорит:
This method exists to be used as a java.util.function.Predicate, filter(Objects::isNull)
И что плохого в том, чтобы писать как раньше?
if (user == null) {}
xpendence Автор
22.01.2018 13:56Вы сами ответили на свой вопрос. А плохого в том, что, к примеру,
поможет избежать дополнительный поиск опечатки в случае если напишете = вместо ==
izzholtik
22.01.2018 17:00+1В каком языке?
Джава так не работает.xpendence Автор
22.01.2018 17:07Да, джава ожидает булевское значение, вот гражданин выше боится передать вместо булевского значения операцию присвоения, я ему и ответил его же опасением.
izzholtik
22.01.2018 17:56+2Вероятно, речь шла немного о другой вещи: «123».equals(var) и var.equals(«123») по-разному обработают ситуацию с var = null.
Сейчас, кстати, в топе висит статья, очень хорошо описывающая мои чувства относительно использования новых, стильных, модных фич языка без нужды. А Objects.isNull() и подобные ещё и читаемость убивают.
mantiscorp
22.01.2018 13:01А не проще ли использовать какой-нибудь SonarLint? Заодно узнаете о себе много нового :)
Vest
22.01.2018 14:46
К сожалению, такое я частенько видел в чужом коде. Это, как мне кажется, наследие от Си. Почему-то люди, забывают, что if в Java работает с Boolean.if( null = user ) {}
Не делайте так, пожалуйста, пишите как в начале. Это несложно.lany
23.01.2018 05:56if в Java работает с Boolean
Нет. Оператор if в Java работает с boolean.
Vest
23.01.2018 16:27Конечно же вы правы, насчёт первой буквы. Я, признаюсь, некоторое время думал как лучше написать, чтобы человек обратил внимание, и на всякий случай указал ссылку на SO, где используется выражение a boolean expression. Да и ошибка в компиляторе выглядит так:
error: incompatible types: OtherClass cannot be converted to boolean
aleksandy
22.01.2018 11:15Согласитесь, не очень. Намного приятнее иметь дело с такой строчкой:
Optional<User> user = Optional.of(repository.findById(userId));
И поймать всё тот же NullPointerException, если пользователя с переданным идентификатором не найдено.
По-моему, правильным решением будет возвращение Optional-а из findById(), а никак не оборачивание его результата.j_wayne
22.01.2018 11:49И даже это не гарантирует, что вместо Optional вам не вернут оттуда null. habrahabr.ru/post/225641
И про этот случай в статье ничего не написано.aleksandy
22.01.2018 20:25За возвращением null из метода, который должен вернуть Optional, Map, Collection, etc., нужно следить всякими анализаторами. А кто так делает, тому металлической линейкой по пальцам во избежание рецидивов.
xpendence Автор
22.01.2018 12:18Вы зря статью не дочитали. Методы обработки .orElse(), .oeElseThrow() и orElseget() как раз страхуют от NPE.
DrBAXA
22.01.2018 12:18Можно и так делать (если дело с библиотекой которая уже есть), но тут точно нужно использовать Optional.ofNullable().
Также здесь
String name = repository.findById(userId).map(user -> user.getName()).orElseThrow(() -> new Exception());
в name вполне может оказатся null и ето для чего придуман Optional.flatMap()
nebachadnezzair
22.01.2018 12:07.map()
Этот метод полностью повторяет аналогичный метод для stream()
Это не так. Optional.map не работает в случае null значения, а Stream.map работает
ElectroGuard
22.01.2018 14:21Увы, но обещание отсутствия NullPointerException не сбылось даже в управляемых языках. А ведь апологеты так хорошо пели о том, что мы больше их никогда не увидим.
xpendence Автор
22.01.2018 14:33Я вот, используя Optional, за последние 2 месяца увидел NPE только один раз.
Vest
22.01.2018 14:51Это как "опционалы в Swift", вы можете обложить свой код всякими ifPresent, isPresent, и не увидите NPE, а потом будете думать, почему у вас данные не возвращаются.
Я сам редко пользуюсь Optionals, потому что предпочитаю не допускать NPE. Для меня ожидаемо работающий код лучше, чем просто работающий код.
potan
22.01.2018 15:43В Scala очень востребованным оказался метод fold, который эквивалентен map + getOrElse.
Может он и в Java есть.
Beyka
22.01.2018 17:08Лично для меня большую часть функционала Optional выполняет элвис-оператор, который есть в kotlin, но до сих пор нет в Java и это печалит, тем более что запись object?.field легче чем optiona.ifPresent(() -> ...). Хотя, конечно, при большой вложенности объектов optional.map будет удобнее чем if (o1 != null && o1.o2 != null && ...)
foal
22.01.2018 17:53.flatMap()
Этот метод делает ровно то же, что и стримовский, с той лишь разницей, что он работает только в том случае, если значение не null.Не совсем так, он разворачивает Optional в отличии от Stream. Но суть да, аналогичная стримовской — избавиться от вложенных контейнеров, e.g.
Optional<Optional<User>>
.
molecularmanvlz
23.01.2018 09:22Фишка Optional не только в том что он NPE-safe но и в том что Optional является монадой и реализует функции map и flatMap что позволяет вам писать код в функциональном стиле. На java это конечно не супер выглядит но в скале все гораздо приятнее.
Например у вас есть 3 имени проперти для подключения к БД (url, user, pass), поэтому вам нужно сходить в какой-то конфиг, взять значения переменных а потом из 3-х переменных сделать одну (ведь вам нужен коннекшн, а не сами логины пароли). В таком случае вы делаете примерно так:
maybe_url, maybe_user, maybe_password все Optional и потом:
Optional maybe_connection = maybe_url.flatMap(url -> maybe_user.flatMap(user -> maybe_pw.map(pw -> connectToDb(url, user, pw)))). Если любая проперти отстутсвтует вы получите Optional.empty на выходе без пробросов исключений
в скале это можно сделать как-то так:
for {
url <- maybe_url
user <- maybe_user
pw <- maybe_pw
connection = connectToDb(url, user, pw)
} yield connection
(разумеется можно навесить и больше, если хочется)
lany
23.01.2018 11:06Уже третье ложное утверждение в комментариях к этой теме:
в том что Optional является монадой
Нет, java.util.Optional не является монадой, так как не соблюдает композицию байндинга (смотрите законы монад).
dougrinch
23.01.2018 17:44Почему не соблюдается? Вроде нормально же
Optional<String> m; Function<String, Optional<Integer>> f; Function<Integer, Optional<Boolean>> g; Optional<Boolean> left = m.flatMap(f).flatMap(g); Optional<Boolean> right = m.flatMap(x -> f.apply(x).flatMap(g));
dougrinch
23.01.2018 19:53Простой тест с перебором всех возможных значений показывает что разный результат будет только когда m == empty() && (f == null || g == null). Честно говоря, мне кажется что тестирование монадических законов на нулевых функциях — это читерство и так делать нельзя.
сам тестpublic class MonadTest { public static void main(String[] args) { List<Optional<String>> ms = asList(ofNullable(null), ofNullable("123"), null); List<Function<String, Optional<Integer>>> fs = asList(s -> ofNullable(null), s -> ofNullable(s.length()), s -> null, null); List<Function<Integer, Optional<Boolean>>> gs = asList(s -> ofNullable(null), i -> ofNullable(i.intValue() == 0), i -> null, null); for (int i = 0; i < ms.size(); i++) { Optional<String> m = ms.get(i); for (int j = 0; j < fs.size(); j++) { Function<String, Optional<Integer>> f = fs.get(j); for (int k = 0; k < gs.size(); k++) { Function<Integer, Optional<Boolean>> g = gs.get(k); try { test(m, f, g); } catch (AssertionError e) { System.out.println(i + " " + j + " " + k); } } } } } private static void test(Optional<String> m, Function<String, Optional<Integer>> f, Function<Integer, Optional<Boolean>> g) { Optional<Boolean> left; boolean npeOnLeft; Optional<Boolean> right; boolean npeOnRight; try { left = m.flatMap(f).flatMap(g); npeOnLeft = false; } catch (NullPointerException e) { left = null; npeOnLeft = true; } try { right = m.flatMap(x -> f.apply(x).flatMap(g)); npeOnRight = false; } catch (NullPointerException e) { right = null; npeOnRight = true; } assertEquals(npeOnLeft, npeOnRight); if (!npeOnLeft) assertEquals(left, right); } }
Melorian
Вы меня, конечно, простите, но зачем здесь официальная и не очень интересная инструкция к уже очень сильно бородатой фиче (особенно, на фоне выхода уже девятой версии Java)?
xpendence Автор
Всё субъективно. Для Вас 3-летняя фича сильно бородатая, а гражданин в комментарии ниже указал, что он пользуется рецептами 20-летней давности. А на том же JavaRush на Java 8 перешли год назад, и сильно сомневаюсь, что они уже учат там пользоваться Optional.