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

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



Класс 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):

  1. Melorian
    /#10628142 / +1

    Вы меня, конечно, простите, но зачем здесь официальная и не очень интересная инструкция к уже очень сильно бородатой фиче (особенно, на фоне выхода уже девятой версии Java)?

    • xpendence
      /#10628320

      Всё субъективно. Для Вас 3-летняя фича сильно бородатая, а гражданин в комментарии ниже указал, что он пользуется рецептами 20-летней давности. А на том же JavaRush на Java 8 перешли год назад, и сильно сомневаюсь, что они уже учат там пользоваться Optional.

  2. dernasherbrezon
    /#10628154

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

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


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

    • mantiscorp
      /#10628304 / +1

      Вы — старпёр :)
      На самом деле, если надо просто проверить User на null, Ваш метод прекрасно работает. Более того, он даже быстрее варианта с Optional, потому что не создаётся/уничтожается объект Optional.
      Но представьте, что у User есть поле address, которое может быть null, в котором есть поле ZIP, которое тоже nullable. Вам надо отобразить это самое последнее поле. Без Optional.map() это будет жуткое количество проверок на null, а с Optional.map() — только одна финальная

      • dernasherbrezon
        /#10628410 / +1

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


        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, если больше, то для экономии всегобщего времени человечества нужно делать проверку вручную.

        • mantiscorp
          /#10628432

          isPresent() вообще не нужно:

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

          • dernasherbrezon
            /#10628478 / -1

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


            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.

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

    • xpendence
      /#10628308

      20 лет? Пишете на Java 1.1?
      Спасибо за информацию, теперь я буду знать, что Вы так пишете.

    • Adverte
      /#10628352

      я придерживаюсь стратегии ставить null в операции сравнения на первое место

      if( user != null ) {} 

      if( null != user ) {} 

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

      • dernasherbrezon
        /#10628362

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

      • xpendence
        /#10628366

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

        И, кстати, вместо

        if (user == null) {}

        давно уже принято использовать
        if (Objects.isNull(user)) {}

        • Azargan
          /#10628498

          Можно уточнить где принято так использовать?
          javadoc для Objects.isNull() говорит:

          This method exists to be used as a java.util.function.Predicate, filter(Objects::isNull)

          И что плохого в том, чтобы писать как раньше?
          if (user == null) {}

          • xpendence
            /#10628500

            Вы сами ответили на свой вопрос. А плохого в том, что, к примеру,

            поможет избежать дополнительный поиск опечатки в случае если напишете = вместо ==

            • izzholtik
              /#10628888 / +1

              В каком языке?
              Джава так не работает.

              • xpendence
                /#10628906

                Да, джава ожидает булевское значение, вот гражданин выше боится передать вместо булевского значения операцию присвоения, я ему и ответил его же опасением.

                • izzholtik
                  /#10629018 / +2

                  Вероятно, речь шла немного о другой вещи: «123».equals(var) и var.equals(«123») по-разному обработают ситуацию с var = null.

                  Сейчас, кстати, в топе висит статья, очень хорошо описывающая мои чувства относительно использования новых, стильных, модных фич языка без нужды. А Objects.isNull() и подобные ещё и читаемость убивают.

                  • xpendence
                    /#10629134 / -1

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

                    • izzholtik
                      /#10629394

                      Вот сейчас бы в 2k18 записывать фразу «а равно двум» как «а == 2». Это не меньшее позёрство, чем счёт с нуля.

      • mantiscorp
        /#10628398

        А не проще ли использовать какой-нибудь SonarLint? Заодно узнаете о себе много нового :)

      • Vest
        /#10628596

        if( null = user ) {}
        К сожалению, такое я частенько видел в чужом коде. Это, как мне кажется, наследие от Си. Почему-то люди, забывают, что if в Java работает с Boolean.
        Не делайте так, пожалуйста, пишите как в начале. Это несложно.

        • lany
          /#10629862

          if в Java работает с Boolean

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

          • Vest
            /#10631040

            Конечно же вы правы, насчёт первой буквы. Я, признаюсь, некоторое время думал как лучше написать, чтобы человек обратил внимание, и на всякий случай указал ссылку на SO, где используется выражение a boolean expression. Да и ошибка в компиляторе выглядит так:

            error: incompatible types: OtherClass cannot be converted to boolean

        • Adverte
          /#10630586

          это был пример ошибки в коде

  3. aleksandy
    /#10628190

    Согласитесь, не очень. Намного приятнее иметь дело с такой строчкой:
    Optional<User> user = Optional.of(repository.findById(userId));

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

    По-моему, правильным решением будет возвращение Optional-а из findById(), а никак не оборачивание его результата.

    • j_wayne
      /#10628268

      И даже это не гарантирует, что вместо Optional вам не вернут оттуда null. habrahabr.ru/post/225641
      И про этот случай в статье ничего не написано.

      • aleksandy
        /#10629338

        За возвращением null из метода, который должен вернуть Optional, Map, Collection, etc., нужно следить всякими анализаторами. А кто так делает, тому металлической линейкой по пальцам во избежание рецидивов.

    • xpendence
      /#10628324

      Вы зря статью не дочитали. Методы обработки .orElse(), .oeElseThrow() и orElseget() как раз страхуют от NPE.

      • molekyla
        /#10628574

        Не застрахуют потому что метод

        Optional.of()

        Вызовет конструктор
        
        private Optional(T value) {
          this.value = Objects.requireNonNull(value);
        }
        

        • xpendence
          /#10628576

          Используйте .ofNullable

          • foal
            /#10628992 / +1

            Мы и используем, но в статье лучше тоже поправить.

            • lany
              /#10629866

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

              • foal
                /#10631536

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

    • DrBAXA
      /#10628326

      Можно и так делать (если дело с библиотекой которая уже есть), но тут точно нужно использовать Optional.ofNullable().
      Также здесь

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

      в name вполне может оказатся null и ето для чего придуман Optional.flatMap()

  4. nebachadnezzair
    /#10628300

    .map()
    Этот метод полностью повторяет аналогичный метод для stream()

    Это не так. Optional.map не работает в случае null значения, а Stream.map работает

  5. ElectroGuard
    /#10628558

    Увы, но обещание отсутствия NullPointerException не сбылось даже в управляемых языках. А ведь апологеты так хорошо пели о том, что мы больше их никогда не увидим.

    • xpendence
      /#10628580

      Я вот, используя Optional, за последние 2 месяца увидел NPE только один раз.

      • Vest
        /#10628614

        Это как "опционалы в Swift", вы можете обложить свой код всякими ifPresent, isPresent, и не увидите NPE, а потом будете думать, почему у вас данные не возвращаются.

        Я сам редко пользуюсь Optionals, потому что предпочитаю не допускать NPE. Для меня ожидаемо работающий код лучше, чем просто работающий код.

  6. potan
    /#10628720

    В Scala очень востребованным оказался метод fold, который эквивалентен map + getOrElse.
    Может он и в Java есть.

  7. Beyka
    /#10628908

    Лично для меня большую часть функционала Optional выполняет элвис-оператор, который есть в kotlin, но до сих пор нет в Java и это печалит, тем более что запись object?.field легче чем optiona.ifPresent(() -> ...). Хотя, конечно, при большой вложенности объектов optional.map будет удобнее чем if (o1 != null && o1.o2 != null && ...)

    • xpendence
      /#10628910

      Круто, напишите об этом статью :)

  8. foal
    /#10629014

    .flatMap()

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

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

  9. molecularmanvlz
    /#10630000

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

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


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

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

      • dougrinch
        /#10631274

        Почему не соблюдается? Вроде нормально же

        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
          /#10631534

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