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



Класс 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
    22.01.2018 10:35
    +1

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


    1. xpendence Автор
      22.01.2018 12:16

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


  1. dernasherbrezon
    22.01.2018 10:38

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

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


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


    1. mantiscorp
      22.01.2018 12:11
      +1

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


      1. 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 за:


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

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


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


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


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


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



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


        1. mantiscorp
          22.01.2018 13:20

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

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


          1. dernasherbrezon
            22.01.2018 13:45
            -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.

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


    1. xpendence Автор
      22.01.2018 12:13

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


    1. Adverte
      22.01.2018 12:33

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

      if( user != null ) {} 

      if( null != user ) {} 

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


      1. dernasherbrezon
        22.01.2018 12:37

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


      1. xpendence Автор
        22.01.2018 12:38

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

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

        if (user == null) {}

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


        1. 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) {}


          1. xpendence Автор
            22.01.2018 13:56

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

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


            1. izzholtik
              22.01.2018 17:00
              +1

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


              1. xpendence Автор
                22.01.2018 17:07

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


                1. izzholtik
                  22.01.2018 17:56
                  +2

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

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


                  1. xpendence Автор
                    22.01.2018 18:47
                    -1

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


                    1. izzholtik
                      22.01.2018 20:53

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


        1. lany
          23.01.2018 05:56

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


      1. mantiscorp
        22.01.2018 13:01

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


      1. Vest
        22.01.2018 14:46

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


        1. lany
          23.01.2018 05:56

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

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


          1. Vest
            23.01.2018 16:27

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

            error: incompatible types: OtherClass cannot be converted to boolean


        1. Adverte
          23.01.2018 13:58

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


  1. aleksandy
    22.01.2018 11:15

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

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

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


    1. j_wayne
      22.01.2018 11:49

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


      1. aleksandy
        22.01.2018 20:24

        -


      1. aleksandy
        22.01.2018 20:25

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


    1. xpendence Автор
      22.01.2018 12:18

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


      1. molekyla
        22.01.2018 14:31

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

        Optional.of()

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


        1. xpendence Автор
          22.01.2018 14:31

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


          1. foal
            22.01.2018 17:46
            +1

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


            1. lany
              23.01.2018 06:03

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


              1. foal
                23.01.2018 19:54

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


    1. DrBAXA
      22.01.2018 12:18

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

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

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


  1. nebachadnezzair
    22.01.2018 12:07

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

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


    1. xpendence Автор
      22.01.2018 12:07

      Да, спасибо, поправил.


  1. ElectroGuard
    22.01.2018 14:21

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


    1. xpendence Автор
      22.01.2018 14:33

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


      1. Vest
        22.01.2018 14:51

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

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


  1. potan
    22.01.2018 15:43

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


  1. Beyka
    22.01.2018 17:08

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


    1. xpendence Автор
      22.01.2018 17:08

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


  1. foal
    22.01.2018 17:53

    .flatMap()

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

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


  1. 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


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


    1. lany
      23.01.2018 11:06

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


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

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


      1. 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));
        


        1. 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);
              }
          }