В этом учебном пособии мы рассмотрим класс Optional, который был представлен в Java 8. Цель класса — предоставить решение на уровне типа для представления опциональных значений вместо null (нулевых) ссылок. Для более глубокого понимания того, почему мы должны обратить внимание на класс Optional, ознакомьтесь с официальной статьей Oracle. Существует несколько способов создания объектов Optional. Чтобы создать пустой объект Optional, нужно просто использовать его статический метод empty(): Обратите внимание, что мы использовали метод isPresent() для проверки наличия значения внутри объекта Optional. Значение присутствует, только если мы создали Optional с non-null (ненулевым) значением. Мы рассмотрим метод isPresent() в следующем разделе. Можно также создать объект Optional с помощью статического метода of(): Однако аргумент, переданный в метод of(), не может быть null. В противном случае мы получим NullPointerException: Но в случае, если мы предполагаем некоторые значения null, то можно использовать метод ofNullable(): Таким образом, если мы передаем null ссылку, это не вызовет исключения, а вернет пустой объект Optional: Когда у нас есть объект Optional, возвращенный из метода или созданный нами, мы можем проверить, есть ли в нем значение или нет, с помощью метода isPresent(): Этот метод возвращает true, если обернутое значение не является null. Также, начиная с Java 11, мы можем сделать обратное с помощью метода isEmpty: Метод ifPresent() позволяет нам запустить некоторый код для обернутого значения, если выяснится, что оно не является null. До метода Optional мы бы сделали следующее: Этот код проверяет, является ли переменная name null или нет, прежде чем приступить к выполнению какого-либо кода над ней. Такой подход занимает много времени, и это не единственная проблема — он также склонен к ошибкам. В самом деле, где гарантия, что после печати этой переменной мы не воспользуемся ею снова, а потом забудем выполнить проверку на null? Это может привести к NullPointerException во время выполнения программы, если в этот код попадет значение null. Когда программа терпит неудачу из-за проблем с вводом, это часто является результатом плохой практики программирования. Optional заставляет нас иметь дело с допускающими null значениями в явном виде, как способ принуждения к хорошей практике программирования. Теперь давайте посмотрим, каким образом приведенный выше код может быть рефакторизован в Java 8. В типичном стиле функционального программирования мы можем выполнить действие над объектом, который действительно присутствует: Метод orElse() используется для получения значения, обернутого внутри экземпляра Optional. Он принимает один параметр, который выступает в качестве значения по умолчанию. Метод orElse() возвращает обернутое значение, если оно присутствует, либо его аргумент в противном случае: Метод orElseGet() аналогичен методу orElse(). Однако вместо того, чтобы принимать значение для возврата, если Optional значение отсутствует, он принимает функциональный интерфейс поставщика, который вызван и возвращает значение вызова: Многим программистам, которые только начинают работать с Optional или Java 8, разница между orElse() и orElseGet() непонятна. На самом деле, создается впечатление, что эти два метода перекрывают друг друга по функциональности. Однако между ними есть тонкое, но очень важное различие, которое может сильно повлиять на производительность нашего кода, если его не понять. Давайте создадим в тестовом классе метод getMyDefault(), который не принимает никаких аргументов и возвращает значение по умолчанию: Давайте рассмотрим два теста и понаблюдаем за их побочными эффектами, чтобы определить, где orElse() и orElseGet() пересекаются, а где отличаются: В приведенном выше примере мы обертываем null текст внутри объекта Optional и пытаемся получить обернутое значение, используя каждый из двух подходов. Побочный эффект таков: В каждом случае вызывается метод getMyDefault(). Выходит, что если обернутое значение отсутствует, то и orElse(), и orElseGet() работают одинаково. Теперь давайте проведем еще один тест, в котором значение присутствует, и в идеале значение по умолчанию даже не должно создаваться: В приведенном выше примере мы больше не оборачиваем значение null, а остальная часть кода остается прежней. Теперь давайте посмотрим на побочный эффект от выполнения этого кода: Обратите внимание, что при использовании orElseGet() для извлечения обернутого значения метод getMyDefault() даже не вызывается, поскольку содержащееся значение присутствует. Однако при использовании orElse(), независимо от того, есть ли обернутое значение или нет, создается объект по умолчанию. Таким образом, в данном случае мы просто создали один лишний объект, который никогда не используется. В этом простом примере создание объекта по умолчанию не требует значительных затрат, поскольку JVM знает, как с ним обращаться. Однако когда такой метод, как getMyDefault(), должен выполнить вызов веб-сервиса или даже запрос к базе данных, затраты становятся весьма очевидными. Метод orElseThrow() следует из orElse() и orElseGet() и добавляет новый подход к обработке отсутствующего значения. Вместо того чтобы возвращать значение по умолчанию, когда обернутое значение отсутствует, он выбрасывает исключение: Здесь пригодятся ссылки на методы в Java 8, чтобы передать в конструктор исключения. Java 10 представила упрощенную версию метода orElseThrow() без аргументов. В случае пустого Optional он выбрасывает исключение NoSuchElementException: Последний способ получения обернутого значения — метод get(): Однако, в отличие от предыдущих трех подходов, get() может вернуть значение, только если обернутый объект не является null; в противном случае он выбрасывает исключение нет такого элемента: Это основной недостаток метода get(). В идеале Optional должен помочь нам избежать таких непредвиденных исключений. Поэтому данный подход противоречит целям Optional и, вероятно, будет упразднен в одном из будущих выпусков. Поэтому рекомендуется использовать другие варианты, которые позволяют нам быть готовыми к случаю null и явно его обрабатывать. Мы можем запустить встроенный тест на нашем обернутом значении с помощью метода filter. Он принимает предикат в качестве аргумента и возвращает объект Optional. Если обернутое значение проходит проверку предикатом, то Optional возвращается как есть. Однако если предикат вернет false, то будет возвращен пустой Optional: Метод filter обычно используется таким образом для отклонения обернутых значений на основе предопределенного правила. Мы могли бы использовать его для отбраковки неправильного формата электронной почты или недостаточно надежного пароля. Давайте рассмотрим другой показательный пример. Допустим, мы хотим купить модем, и нас интересует только его цена. С определенного сайта нам приходят push-уведомления о ценах на модемы, и они сохраняются в объектах: Затем мы передаем эти объекты некоторому коду, единственная цель которого — проверить, находится ли цена модема в пределах нашего бюджета. Теперь давайте посмотрим на код без Optional: Обратите внимание на то, как много кода нам приходится писать для достижения этой цели, особенно в условии if. Единственная часть условия if, которая критична для приложения, — это последняя проверка диапазона цен; остальные проверки носят вспомогательный характер: Кроме того, о проверках на null можно забыть надолго, не получив ни одной ошибки во время компиляции. Теперь давайте рассмотрим вариант с Optional#filter: Вызов map используется для трансформации одного значения в другое. Следует помнить, что эта операция не изменяет исходное значение. В нашем случае мы получаем объект цены из класса Model. Метод map() будет подробно рассмотрен в следующем разделе. Во-первых, если в этот метод передается объект null, то никаких проблем не предвидится. Во-вторых, единственная логика, которую мы прописываем внутри его тела, это именно то, что описывает название метода — проверка ценового диапазона. Об остальном позаботится Optional: Предыдущий подход обещает проверить диапазон цен, но при этом должен сделать нечто большее, чтобы защититься от присущей ему хрупкости. Поэтому мы можем использовать метод filter, чтобы заменить ненужные операторы if и отбросить нежелательные значения. В предыдущем разделе мы рассмотрели, как отклонить или принять значение на основе фильтра. Мы можем использовать аналогичный синтаксис для преобразования значения Optional с помощью метода map(): В этом примере мы оборачиваем список строк внутри объекта Optional и используем его метод map для выполнения действия над содержащимся в нем списком. Оно заключается в получении размера списка. Метод map возвращает результат вычислений, завернутый внутрь Optional. Затем мы должны вызвать соответствующий метод для возвращенного Optional, чтобы получить это значение. Обратите внимание, что метод filter просто выполняет проверку значения и возвращает Optional, описывающий это значение, только если оно соответствует заданному предикату. В противном случае возвращается пустой Optional. Метод map берет существующее значение, выполняет вычисления, используя его, и возвращает результат вычислений, обернутый в объект Optional: Мы можем соединить map и filter вместе, чтобы сделать что-то более мощное. Допустим, нам нужно проверить правильность пароля, введенного пользователем. Можно очистить пароль с помощью трансформации map и проверить его правильность с помощью filter: Как мы видим, без предварительной очистки ввода он будет отфильтрован. Однако пользователи могут считать само собой разумеющимся, что начальные и конечные пробелы — это всё входные данные. Поэтому мы преобразуем измененный пароль в очищенный с помощью map, прежде чем отфильтровать неправильные пароли. Подобно методу map(), у нас также есть метод flatMap() в качестве альтернативы для трансформации значений. Разница в том, что map преобразует значения только тогда, когда они извлечены (развернуты), в то время как flatMap берет обернутое значение и разворачивает его перед трансформацией. Ранее мы создавали простые объекты String и Integer для обертывания в экземпляр Optional. Теперь, зачастую, мы будем получать эти объекты от асессора сложного объекта. Чтобы лучше понять разницу, давайте рассмотрим объект Person, который принимает данные человека, такие как имя, возраст и пароль: Обычно мы создаем такой объект и оборачиваем его в Optional, как мы это делали со String. В качестве альтернативы он может быть возвращен нам другим вызовом метода: Обратите внимание, что когда мы обернем объект Person, он будет содержать вложенные экземпляры Optional: Здесь мы пытаемся извлечь атрибут name объекта Person, чтобы выполнить утверждение. Обратите внимание, как мы достигаем этого с помощью метода map() в третьем операторе, а затем заметьте, как мы делаем то же самое с помощью метода flatMap() после этого. Ссылка на метод Person::getName похожа на вызов String::trim, который мы использовали в предыдущем разделе для очистки пароля. Единственное отличие заключается в том, что getName() возвращает Optional, а не String, как операция trim(). Это, в сочетании с тем, что трансформация map оборачивает результат в объект Optional, и приводит к появлению вложенного Optional. Поэтому во время применения метода map() необходимо добавить дополнительный вызов для получения значения перед использованием трансформированного значения. Таким образом, обертка Optional будет удалена. Эта операция выполняется неявно при использовании flatMap. Иногда нам может понадобиться получить первый непустой объект Optional из нескольких Optional. В таких случаях было бы очень удобно использовать метод типа orElseOptional(). К сожалению, в Java 8 такая операция напрямую не поддерживается. Давайте сначала представим несколько методов, которые мы будем использовать на протяжении всего этого раздела: Для того чтобы выстроить цепочку из нескольких объектов Optional и получить первый непустой объект в Java 8, мы можем использовать Stream API: Недостатком этого подхода является то, что все наши методы get выполняются всегда, независимо от того, где в Stream появляется непустой Optional. Если мы хотим лениво оценить методы, переданные в Stream.of(), нам нужно использовать ссылку на метод и интерфейс Supplier: В случае если нам нужно использовать методы, принимающие аргументы, мы должны прибегнуть к лямбда-выражениям: Часто мы хотим вернуть значение по умолчанию в случае, если все цепочки Optional пусты. Мы можем сделать это, просто добавив вызов orElse() или orElseGet(): Релиз Java 9 добавил еще больше новых методов в Optional API: метод or() для предоставления поставщика, который создает альтернативный Optional; ifPresentOrElse() метод, позволяющий выполнить действие, если Optional присутствует, или другое действие, если нет; метод stream() для преобразования Optional в Stream. Вот полный текст статьи для дальнейшего чтения. Наконец, давайте рассмотрим привлекательный, но опасный способ использования Optional: передача параметра Optional методу. Представьте, что у нас есть список Person, и мы хотим, чтобы метод искал в этом списке людей с заданным именем. Кроме того, желательно, чтобы этот метод находил записи, имеющие по крайней мере определенный возраст, если он указан. Поскольку этот параметр является опциональным, мы пришли к такому методу: Затем мы публикуем наш метод, и другой разработчик пытается его использовать: Теперь разработчик выполняет свой код и получает NullPointerException. Мы вынуждены осуществлять проверку на null нашего опционального параметра, что противоречит нашей первоначальной цели избежать подобной ситуации. Вот некоторые возможности, которые можно было бы сделать, чтобы справиться с этим лучше: Здесь параметр по-прежнему является опциональным, но мы обрабатываем его только в одной проверке. Другим возможным вариантом является создание двух перегруженных методов: Таким образом, мы предлагаем четкий API с двумя методами, делающими разные вещи (хотя они имеют общую имплементацию). Итак, существуют решения, позволяющие избежать использования Optional в качестве параметров метода. Намерением Java было при выпуске Optional использовать его в качестве возвращаемого типа, таким образом указывая, что метод может вернуть пустое значение. На самом деле, практика использования Optional в качестве параметра метода даже не рекомендуется некоторыми инспекторами кода. Как уже говорилось выше, Optional предназначен для использования в качестве возвращаемого типа. Пытаться использовать его в качестве типа поля не рекомендуется. Кроме того, использование Optional в сериализуемом классе приведет к возникновению исключения NotSerializableException. В нашей статье Java Optional как возвращаемый тип вопросы сериализации рассматриваются подробнее . А в статье Использование Optional в Jackson мы объясняем, что происходит при сериализации полей Optional, и также приводим несколько обходных путей для достижения желаемых результатов. В этой статье мы охватили большинство важнейших фич класса Optional в Java 8. Вкратце исследовали некоторые причины, побудившие нас использовать Optional вместо явной проверки на null и валидации ввода. Также узнали, как получить значение Optional или значение по умолчанию, если оно пустое, с помощью методов get(), orElse() и orElseGet() (и заметили важную разницу между двумя последними). Затем рассмотрели, как трансформировать или фильтровать наши Optional с помощью map(), flatMap() и filter(). Обсудили, что предлагает текучий API Optional, позволяющий легко соединять различные методы в цепочку. Наконец, увидели, почему использование Optional в качестве параметров метода является плохой идеей и как этого избежать. Исходный код всех примеров, приведенных в статье, доступен на GitHub. Приглашаем все желающих на открытое занятие «Объектно-ориентированное и функциональное программирование», на котором разберем отличия между этими двумя подходами на максимально простом уровне. Примеры рассмотрим на языке Java. Во второй части занятия вас ждет подробное описание особенностей специализации Java-разработчик. Регистрация — по ссылке.  Переводчик, редактор