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

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



Класс 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 году просто обязательно.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 50
  • +2
    Вы меня, конечно, простите, но зачем здесь официальная и не очень интересная инструкция к уже очень сильно бородатой фиче (особенно, на фоне выхода уже девятой версии Java)?
    • +1
      Всё субъективно. Для Вас 3-летняя фича сильно бородатая, а гражданин в комментарии ниже указал, что он пользуется рецептами 20-летней давности. А на том же JavaRush на Java 8 перешли год назад, и сильно сомневаюсь, что они уже учат там пользоваться Optional.
    • +10
      User user;
      if (Objects.nonNull(user =  repository.findById(userId))) {
      (остальная борода пишется тут)
      }

      Действительно, не очень. Зовите меня старпёром, но последние 20 лет я пишу так, и проблем не было:


      User user = repository.findById(userId);
      if( user != null ) {
      (остальная борода пишется тут)
      }
      • +2
        Вы — старпёр :)
        На самом деле, если надо просто проверить User на null, Ваш метод прекрасно работает. Более того, он даже быстрее варианта с Optional, потому что не создаётся/уничтожается объект Optional.
        Но представьте, что у User есть поле address, которое может быть null, в котором есть поле ZIP, которое тоже nullable. Вам надо отобразить это самое последнее поле. Без Optional.map() это будет жуткое количество проверок на null, а с Optional.map() — только одна финальная
        • 0

          Мне было действительно интересно насколько я старпёр. Так что я это померил (ради лулзов конечно же).


          DISCLAIMER: автор не несёт ответственности за причинённый кому либо моральный ущерб


          Я решил сравнить время написания кода проверок на null и время потраченное компьютером на выполнения кода с Optional.


                      if (address != null && address.getZip() != null) {
                          System.out.println(address.getZip().getValue());
                      }

                      if (op.map(cur -> cur.getZip()).isPresent()) {
                          (остальная борода пишется тут)
                      }

          Проверки почти одинаковые, и даже если немного разные, то это не влияет на суть исследования.
          Секундомер на столе показал, что я могу написать условие на null за:


          1. 23s
          2. 16s
          3. 14s

          Будем брать самый первый результат, потому что в реальной жизни приходится часто опечатываться. Даже с помощью IDE я опечатался 2 раза.


          Теперь замерим сколько будет выполнять тот и другой код. Простой цикл с замером времени в миллисекундах. Кто хочет, может подключить JMH и получить титул "зануда месяца".


          Обычные проверки дали:
          38 ms


          Проверки с Optional:
          88 ms


          Настало время анализа. Для этого нужен график. Например, такой:



          Из него следует, что выигрыш в использовании обычных проверок на null наступает в районе 461 вызова. Т.е. если Ваш код вызывается меньше, чем 461, то выгоднее использовать Optional, если больше, то для экономии всегобщего времени человечества нужно делать проверку вручную.

          • +1
            isPresent() вообще не нужно:
            System.out.println(user.map(User::getAddress).map(Address::getZip).orElse(""));
            
            согласитесь, что код выглядит намного чище за счёт полного устранения проверок на null.
            • –2

              Вкусовщина. Мне недавно пришел вот такой код на ревью:


              Optional.ofNullable(toKill).ifPresent( p -> {
                  do something with p.
              })

              Автор, когда писал этот код, просто хотел сделать:


              if( toKill != null ) {
                  do something with toKill.
              }

              Но вместо этого он заставил всех потомков и других разработчиков держать в голове целых 3 конструкции:


              1. Optional.ofNullable и его интерфейс
              2. ifPresent, которое как раз делает преобразование в is not null в is present
              3. переименование toKill в p.

              Читаемость такого кода так себе.

        • –4
          20 лет? Пишете на Java 1.1?
          Спасибо за информацию, теперь я буду знать, что Вы так пишете.
          • +1
            я придерживаюсь стратегии ставить null в операции сравнения на первое место
            if( user != null ) {} 

            if( null != user ) {} 

            поможет избежать дополнительный поиск опечатки в случае если напишете = вместо ==
            if( null = user ) {} 
            • 0

              Мой, опять же старпёрский, инструмент Eclipse говорит что "cannot convert from Address to boolean". Проблем не будет если ставить null в конце.

              • –3
                Язык развивается, старые рецепты заменяются новыми. Также, если Вы хотите работать с последними версиями популярных фреймворков, Вам будет необходимо изучить Optional, а если хотите успешно с ними работать, то выучить его нужно будет хорошо.
                Также, как уже было указано в статье, Optional очень хорошо чистит код и делает его более читабельным. Ведь такого рода проверки на null являются служебными и только мешают видеть суть кода.

                И, кстати, вместо
                if (user == null) {}

                давно уже принято использовать
                if (Objects.isNull(user)) {}
                • +3
                  Можно уточнить где принято так использовать?
                  javadoc для Objects.isNull() говорит:
                  This method exists to be used as a java.util.function.Predicate, filter(Objects::isNull)

                  И что плохого в том, чтобы писать как раньше?
                  if (user == null) {}
                  • –3
                    Вы сами ответили на свой вопрос. А плохого в том, что, к примеру,
                    поможет избежать дополнительный поиск опечатки в случае если напишете = вместо ==
                    • НЛО прилетело и опубликовало эту надпись здесь
                      • –3
                        Да, джава ожидает булевское значение, вот гражданин выше боится передать вместо булевского значения операцию присвоения, я ему и ответил его же опасением.
                        • НЛО прилетело и опубликовало эту надпись здесь
                          • –2

                            Вот видите, Вы сходу допустили синтаксическую ошибку, подтвердив мои слова :)

                            • НЛО прилетело и опубликовало эту надпись здесь
                  • +5

                    Нет, не принято.

                  • 0
                    А не проще ли использовать какой-нибудь SonarLint? Заодно узнаете о себе много нового :)
                    • +1
                      if( null = user ) {}
                      К сожалению, такое я частенько видел в чужом коде. Это, как мне кажется, наследие от Си. Почему-то люди, забывают, что if в Java работает с Boolean.
                      Не делайте так, пожалуйста, пишите как в начале. Это несложно.
                      • +2
                        if в Java работает с Boolean

                        Нет. Оператор if в Java работает с boolean.

                        • 0
                          Конечно же вы правы, насчёт первой буквы. Я, признаюсь, некоторое время думал как лучше написать, чтобы человек обратил внимание, и на всякий случай указал ссылку на SO, где используется выражение a boolean expression. Да и ошибка в компиляторе выглядит так:
                          error: incompatible types: OtherClass cannot be converted to boolean
                        • 0
                          это был пример ошибки в коде
                    • +3
                      Согласитесь, не очень. Намного приятнее иметь дело с такой строчкой:
                      Optional<User> user = Optional.of(repository.findById(userId));

                      И поймать всё тот же NullPointerException, если пользователя с переданным идентификатором не найдено.

                      По-моему, правильным решением будет возвращение Optional-а из findById(), а никак не оборачивание его результата.
                      • 0
                        И даже это не гарантирует, что вместо Optional вам не вернут оттуда null. habrahabr.ru/post/225641
                        И про этот случай в статье ничего не написано.
                        • 0
                          -
                          • 0
                            За возвращением null из метода, который должен вернуть Optional, Map, Collection, etc., нужно следить всякими анализаторами. А кто так делает, тому металлической линейкой по пальцам во избежание рецидивов.
                          • –2
                            Вы зря статью не дочитали. Методы обработки .orElse(), .oeElseThrow() и orElseget() как раз страхуют от NPE.
                            • 0
                              Не застрахуют потому что метод
                              Optional.of()

                              Вызовет конструктор
                              
                              private Optional(T value) {
                                this.value = Objects.requireNonNull(value);
                              }
                              
                              • 0
                                Используйте .ofNullable
                                • +1
                                  Мы и используем, но в статье лучше тоже поправить.
                                  • 0

                                    Кстати, в исходниках нашего проекта 116 вхождений ofNullable и 46 вхождений of. В общем-то of тоже нужен частенько.

                                    • 0

                                      Конечно, нужен. Вот только для обертывания легаси кода, как в примерах у комментируемой статьи, лучше использовать ofNullable.

                            • 0
                              Можно и так делать (если дело с библиотекой которая уже есть), но тут точно нужно использовать Optional.ofNullable().
                              Также здесь
                              String name = repository.findById(userId).map(user -> user.getName()).orElseThrow(() -> new Exception());

                              в name вполне может оказатся null и ето для чего придуман Optional.flatMap()
                            • +1
                              .map()
                              Этот метод полностью повторяет аналогичный метод для stream()

                              Это не так. Optional.map не работает в случае null значения, а Stream.map работает
                            • +2
                              Увы, но обещание отсутствия NullPointerException не сбылось даже в управляемых языках. А ведь апологеты так хорошо пели о том, что мы больше их никогда не увидим.
                              • –2
                                Я вот, используя Optional, за последние 2 месяца увидел NPE только один раз.
                                • +1
                                  Это как "опционалы в Swift", вы можете обложить свой код всякими ifPresent, isPresent, и не увидите NPE, а потом будете думать, почему у вас данные не возвращаются.

                                  Я сам редко пользуюсь Optionals, потому что предпочитаю не допускать NPE. Для меня ожидаемо работающий код лучше, чем просто работающий код.
                              • 0
                                В Scala очень востребованным оказался метод fold, который эквивалентен map + getOrElse.
                                Может он и в Java есть.
                                • 0
                                  Лично для меня большую часть функционала Optional выполняет элвис-оператор, который есть в kotlin, но до сих пор нет в Java и это печалит, тем более что запись object?.field легче чем optiona.ifPresent(() -> ...). Хотя, конечно, при большой вложенности объектов optional.map будет удобнее чем if (o1 != null && o1.o2 != null && ...)
                                  • 0
                                    Круто, напишите об этом статью :)
                                  • 0
                                    .flatMap()

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

                                    Не совсем так, он разворачивает Optional в отличии от Stream. Но суть да, аналогичная стримовской — избавиться от вложенных контейнеров, e.g. Optional<Optional<User>>.

                                    • 0

                                      Фишка 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


                                      (разумеется можно навесить и больше, если хочется)

                                      • 0

                                        Уже третье ложное утверждение в комментариях к этой теме:


                                        в том что Optional является монадой

                                        Нет, java.util.Optional не является монадой, так как не соблюдает композицию байндинга (смотрите законы монад).

                                        • 0
                                          Почему не соблюдается? Вроде нормально же
                                          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));
                                          
                                          • 0
                                            Простой тест с перебором всех возможных значений показывает что разный результат будет только когда 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);
                                                }
                                            }
                                            

                                            • 0

                                              Не совсем красиво я выразился, пожалуй. Нулевые функции — это читерство, а вот функции, принимающие или возвращающие null — вполне законны. Очевидно, что flatMap должно соответствовать операции bind. Вопрос: какая операция соответствует операции return? Предположим, что Optional.ofNullable. Тогда f = Optional::of нарушает закон (return v) >>= f ≡ f v для v = null. В терминах Java слева Optional.ofNullable(null).flatMap(f) => empty(), а вот f.apply(null) — NPE. Ладно, предположим, что return — это Optional.of. Тогда возьмём f = Optional::ofNullable и снова получим несоответствие.


                                              Более интересные эффекты проявляются с операцией map (аналог хаскеловского fmap). Пусть есть две функции:


                                              UnaryOperator<Object> f = x -> null;
                                              UnaryOperator<Object> g = x -> "foo";

                                              Возьмём произвольный непустой Optional opt. Композиция fmap подразумевает, что opt.map(f).map(g) эквивалентно opt.map(g.compose(f)), а это не так: первый — это всегда empty, а второй — это Optional.of("foo").

                                              • +1
                                                Я, конечно, тоже молодец, слишком доверчивый. Раз написано про ошибку в композиции, то проверил именно ее. А получилось что это как раз единственный закон из трех, который работает полностью.

                                                Но вообще, мне кажется что все это некорректно. Если за базу мы берем haskell, то с нулами получается интересно. Ведь там их нет, именно за этим и нужен Maybe. Соответственно, логично предположить что если функция принимает какой-то тип, то она принимает именно этот тип, а не null. И тогда получается что (return v) >>= f ≡ f v для случая v = null просто не имеет смысла, т.к. конструкции (return null) и (f null) не существуют. На простой map, я думаю, данное ограничение тоже распространяется.

                                                Так что еще большой вопрос является ли Optional монадой в строгом значении.

                                                Но вообще, по поводу монад у меня есть другая мысль. Рискну быть заминусованным, но предположу что когда про монады говорит кто-нибудь, кто не является профессиональным фп-программистом (что-бы это не значило), он имеет в виду не строгий математический объект, а некую monad-like «контейнерную» абстракцию, позволяющую легко работать напрямую с содержимым в «нормальных» сценариях. А что при этом оно почти всегда разваливается на границах — да кому какое дело?! В конце концов, не надо мешать нулы и опшионалы.

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