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.
    • НЛО прилетело и опубликовало эту надпись здесь
        +2
        Вы — старпёр :)
        На самом деле, если надо просто проверить User на null, Ваш метод прекрасно работает. Более того, он даже быстрее варианта с Optional, потому что не создаётся/уничтожается объект Optional.
        Но представьте, что у User есть поле address, которое может быть null, в котором есть поле ZIP, которое тоже nullable. Вам надо отобразить это самое последнее поле. Без Optional.map() это будет жуткое количество проверок на null, а с Optional.map() — только одна финальная
        • НЛО прилетело и опубликовало эту надпись здесь
            +1
            isPresent() вообще не нужно:
            System.out.println(user.map(User::getAddress).map(Address::getZip).orElse(""));
            
            согласитесь, что код выглядит намного чище за счёт полного устранения проверок на null.
            • НЛО прилетело и опубликовало эту надпись здесь
          –4
          20 лет? Пишете на Java 1.1?
          Спасибо за информацию, теперь я буду знать, что Вы так пишете.
            +1
            я придерживаюсь стратегии ставить null в операции сравнения на первое место
            if( user != null ) {} 

            if( null != user ) {} 

            поможет избежать дополнительный поиск опечатки в случае если напишете = вместо ==
            if( null = user ) {} 
            • НЛО прилетело и опубликовало эту надпись здесь
                –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 работает
                                0
                                Да, спасибо, поправил.
                                +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 «контейнерную» абстракцию, позволяющую легко работать напрямую с содержимым в «нормальных» сценариях. А что при этом оно почти всегда разваливается на границах — да кому какое дело?! В конце концов, не надо мешать нулы и опшионалы.

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

                                        Самое читаемое