Перейти к содержимому

Optional как выполнить метод map

  • автор:

Большой гайд по Optional в Java

Разбираемся, как уменьшить шанс получить NullPointerException, используя класс Optional.

15 мая 2022 · 11 минуты на чтение

Преде тем, как разбираться в Optional , давайте выясним какую проблему он решает. Представим, что у нас есть класс UserRepository , который работает с хранилищем наших пользователей, в данном примере это будет обычная HashMap .

Допустим, мы хотим найти пользователя в базе данных по идентификатору. После чего нам требуется вывести на консоль длину имени пользователя. На первый взгляд это простой и безобидный пример.

final Person person = personRepository.findById(1L); final String firstName = person.getFirstName(); System.out.println("Длина твоего имени: " + firstName.length());

Выполнив этот код, мы можем получить исключение:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "dev.struchkov.example.optional.Person.getFirstName()" because "person" is null at dev.struchkov.example.optional.Main.test(Main.java:18) at dev.struchkov.example.optional.Main.main(Main.java:13)

Почему это могло произойти? Дело в том, что мы не учли того факта, что пользователя с идентификатором 1L может не быть в нашей HashMap . И у нас нет варианта, кроме как вернуть null .

Перепишем наш пример с использованием Optional и посмотрим, что изменится:

package dev.struchkov.example.optional; import java.util.Map; import java.util.Optional; public class PersonRepository < private final Mappersons; public PersonRepository(Map persons) < this.persons = persons; >public Optional findById(Long id) < return Optional.ofNullable(persons.get(id)); >>
final Optional optPerson = personRepository.findById(1L); if (optPerson.isPresent()) < final Person person = optPerson.get(); final String firstName = person.getFirstName(); if (firstName != null) < System.out.println("Длина твоего имени: " + firstName.length()); >>

Вы скажете: это не выглядит проще, даже более того, добавилась еще одна строка. Но на самом деле случилось концептуально важное событие, вы точно знаете что метод findById() возвращает контейнер, в котором объекта может не быть. Больше вы не ожидаете внезапного null значения, если только разработчик этого метода не сумашедший, ведь ничто не мешает ему вернуть null вместо Optional ��

Здесь мы с вами увидели два основных метода: isPresent() возвращает true , если внутри есть объект, а метод get() возвращает этот объект.

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

final Optional optPerson = personRepository.findById(1L); optUser.map(person -> person.getFirstName()) .ifPresent( firstName -> System.out.println("Длина твоего имени: " + firstName.length()) );

Мы воспользовались методом map() , который преобразует наш Optional в Optional другого типа. В данном случае мы получили фамилию пользователя, то есть преобразовали Optional в Optional .

Вот такой вариант показывает это нагляднее:

final Optional optPerson = personRepository.findById(1L); final Optional optFirstName = optPerson.map(user -> user.getFirstName()); optFirstName.ifPresent( firstName -> System.out.println("Длина твоего имени: " + firstName.length()) );

А дальше мы вызвали метод ifPresent() , в котором мы вызвали вывод на консоль, можно еще чуть-чуть сократить код:

personRepository.findById(1L) .map(User::getFirstName) .ifPresent( firstName -> System.out.println("Длина твоего имени: " + firstName.length()) );

Методы Optional

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

Cоздание Optional

У данного класса нет конструкторов, но есть три статических метода, которые и создают экземпляры класса,

Метод Optional.ofNullable(T t)

Optional.ofNullable принимает любой тип и создает контейнер. Если в параметры этого метода передать null , то будет создан пустой контейнер.

Используйте этот метод, когда есть вероятность, что упаковываемый объект может иметь null значение.

public class PersonRepository < private final Mappersons; public PersonRepository(Map persons) < this.persons = persons; >public Optional findById(Long id) < return Optional.ofNullable(persons.get(id)); >>

В этом примере мы используем ofNullable() , потому что метод Map#get() может вернуть нам null .

Метод Optional.of(T t)

Этот метод аналогичен Optional.ofNullable , но если передать в параметр null значение, то получим старый добрый NullPointerException .

Используйте этот метод, когда точно уверены, что упаковываемое значение не должно быть null .

public class PersonRepository < private final Mappersons; public PersonRepository(Map persons) < this.persons = persons; >public Optional findByLogin(String login) < for (Person person : persons.values()) < if (person.getLogin().equals(login)) < return Optional.of(person); >> return Optional.empty(); > >

Новый метод позволяет найти пользователя по логину. Для этого мы проходимся по значениям Map и сравниваем логины пользователей с переданным, как только находим совпадение вызываем метод Optional.of() , потому что мы уверены, что такой объект существует.

Метод Optional.empty()

Но что делать, если пользователь с переданным логином не был найден? Метод все равно должен вернуть Optional .

Можно вызвать Optional.ofNullable(null) и вернуть его, но лучше воспользоваться методом Optional.empty() , который возвращает пустой Optional .

Обратите внимание на последний пример, там мы как раз используем Optional.empty() .

Получение содержимого

Мы разобрали методы, которые позволяют нам создавать объект Optional . Теперь изучим методы, которые позволят нам доставать значения из контейнера.

Метод isPresent()

Прежде, чем достать что-то, неплохо убедиться, что это что-то там действительно есть. И с этим нам поможет метод isPresent() , который возвращает true , если в контейнере есть объект и false в противном случае.

Optional optPerson = personRepository.findById(1L); if(optPerson.isPresent()) < // если пользователь найден, то выполняем этот блок кода >

Фактически это обычная проверка, как если бы мы сами написали if (value != null) . И если зайти в реализацию метода isPresent() , то это мы там и увидим:

public boolean isPresent()
Метод isEmpty()

Метод isEmpty() это противоположность методу isPresent() . Метод вернет true , если объекта внутри нет и false в противном случае. Думаю, вы уже догадались, как выглядит реализация этого метода:

public boolean isEmpty()
Метод get()

После того, как вы убедились в наличии объекта с помощью предыдущих методов, вы можете смело достать объект из контейнера с помощью метода get() .

Optional optPerson = personRepository.findById(1L); if(optPerson.isPresent()) < final Person person = optPerson.get(); // остальной ваш код >

Конечно, вы можете вызвать метод get() и без проверки. Но если объекта там не окажется, то вы получите NoSuchElementException .

Метод ifPresent()

Помимо метода isPresent() , имеется метод ifPresent() , который принимает в качестве аргумента функциональный интерфейс Consumer . Это позволяет нам выполнить какую-то логику над объектом, если объект имеется.

Давайте с помощью этого метода выведем имя и фамилию пользователя на консоль.

personRepository.findById(id).ifPresent( person -> System.out.println(person.getFirstName() + " " + person.getLastName()) );
Метод ifPresentOrElse()

Метод ifPresent() ничего не сделает, если у вас нет объекта, но если вам и в этом случае необходимо выполнить какой-то код, то используйте метод ifPresentOrElse() , который принимает в качестве параметра еще и функциональный интерфейс Runnable .

Возьмем наш пример выше и выведем в консоль Иван Иванов, если пользователь не был найден.

personRepository.findById(id).ifPresentOrElse( person -> System.out.println(person.getFirstName() + " " + person.getLastName()), () -> System.out.println("Иван Иванов") );
Метод orElse()

Метод делает ровно то, что ожидается от его названия: возвращает значение в контейнере или значение по-умолчанию, которое вы указали.

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

public Person getPersonOrAnon(Long id)

Используйте этот метод, когда хотите вернуть значение по умолчанию, если контейнер пустой.

Некоторым приходит в голову довольно странная конструкция: objectOptional.orElse(null) , которая лишает использование Optional всякого смысла. Никогда так не делайте, и бейте по рукам тем, кто так делает.

Метод orElseGet()

Метод похож на предыдущий, но вместо возвращения значения, он выполнит функциональный интерфейс Supplier .

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

public Person getPersonOrAnonWithLog(Long id) < return personRepository.findById(id) .orElseGet(() ->< System.out.println("Пользователь не был найден, отправляем анонимного"); return new Person(-1L, "anon", "anon", "anon"); >); >

Используйте этот метод, когда вам не достаточно просто вернуть какое-то дефолтное значение, но и требуется выполнить какую-то более сложную логику.

Метод orElseThrow()

Этот метод вернет объект если он есть, в противном случае выбросит стандартное исключение NoSuchElementException(«No value present») .

Метод orElseThrow(Supplier s)

Этот метод позволяет вернуть любое исключение вместо стандартного NoSuchElementException , если объекта нет.

public Person getPersonOrThrow(Long id) < return personRepository.findById(id) .orElseThrow(() ->new NotFoundException("Пользователь не найден")); >

Элегантный orElseThrow

Как вам такой вариант использования orElseThrow()

public Person getPersonOrElegantThrow(Long id) < return personRepository.findById(id) .orElseThrow(notFoundException("Пользователь не найден", id)); >

На мой взгляд это самый эстетический вариант использования. Для этого вам нужно в классе исключения добавить метод, который будет возращать Supplier .

Вот пример NotFoundException , мы используем два метода notFoundException : один позволяет передать просто строку, а другой использует MessageFormat.format() , чтобы формировать сообщение с параметрами.

import java.text.MessageFormat; import java.util.function.Supplier; public class NotFoundException extends RuntimeException < public NotFoundException(String message) < super(message); >public NotFoundException(String message, Object. args) < super(MessageFormat.format(message, args)); >public static Supplier notFoundException(String message, Object. args) < return () ->new NotFoundException(message, args); > public static Supplier notFoundException(String message) < return () ->new NotFoundException(message); > >

Преобразование

Optional также имеет ряд методов, которые могет быть знакомы вам по стримам: map , filter и flatMap . Они имеют такие же названия, и делают примерно то же самое.

Метод filter()

Если внутри контейнера есть значение и оно удовлетворяет переданному условию в виде функционального интерфейса Predicate , то будет возвращен новый объект Optional с этим значением, иначе будет возвращен пустой Optional .

Например, мы сделаем метод, который возвращает только взрослых пользователей по id.

public Optional getAdultById(Long id) < return personRepository.findById(id) .filter(person ->person.getAge() > 18); >

Используйте его, когда вам нужен контейнер, объект в котором удвлетворят какому-то условию.

Метод map()

Если внутри контейнера есть значение, то к значению применяется переданная функция, результат помещается в новый Optional и возвращается, в случае отсутствия значения будет возвращен пустой контейнер. Для преобразования используется функциональный интерфейс Function .

Сделаем метод, который по идентификатору пользователя возвращает Optional , содержащий имя и фамилию этого пользователя.

public Optional getFirstAndLastNames(Long id) < return personRepository.findById(id) .map(person ->person.getFirstName() + " " + person.getLastName()); >

Используйте этот метод, когда необходимо преобразовать объект внутри контейнера в другой объект.

Метод flatMap()

Как уже было сказано, map() оборачивает возвращаемый результат лямбды Function . Но что, если эта лямбда у нас будет возвращать уже обернутый результат, у нас получится Optional> . С таким дважды упакованным объектом будет сложно работать.

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

Optional> optUserPhoneNumber = personRepository.findById(1L) .map(person -> < OptionaloptPhoneNumber = phoneNumberRepository.findByPersonId(person.getId()); return optPhoneNumber; >);

В таком случае используйте flatMap() , он позволит вам избавиться от лишнего контейнера.

Optional optUserPhoneNumber = personRepository.findById(1L) .flatMap(person -> < OptionaloptPhoneNumber = phoneNumberRepository.findByLogin(user.getLogin()); return optPhoneNumber; >);
Метод stream()

В Java 9 в Optional был добавлен новый метод stream() , который позволяет удобно работать со стримом от коллекции Optional элементов.

Допустим, вы запрашивали множество пользователей по различным идентификаторам, а в ответ вам возвращался Optional . Вы сложили полученные объекты в коллекцию, и теперь с ними нужно как-то работать. Можно создать стрим от этой коллекции, и используя метод flatMap() стрима и метод stream() у Optional , чтобы получить стрим существующих пользователей. При этом все пустые контейнеры будут отброшены.

final List> optPeople = new ArrayList<>(); final List people = optPeople.stream() .flatMap(optItem -> optItem.stream()) .toList();
Метод or()

Начиная с Java 11 добавили новый метод or() . Он позволяет изменить пустой Optional передав новый объект, раньше так сделать было нельзя.

Важно понимать, что этот метод не изменяет объект Optional , а создает и возвращает новый.

Например мы запрашиваем пользователя по идентификатору, и если его нет в хранилище, то мы передаем Optional анонимного пользователя.

public Optional getPersonOrAnonOptional(Long id) < return personRepository.findById(id) .or(() ->Optional.of(new Person(-1L, "anon", "anon", "anon", 0L))); >

Комбинирование методов

Все перечисленные методы возвращают в ответ Optional , поэтому вы можете составлять из них цепочки, прямо как у стримов.

Пример оторванный от реальности, но иллюстрирующий цепочку методов:

final LocalDateTime now = LocalDateTime.now(); final DayOfWeek dayWeek = Optional.of(now) .map(LocalDateTime::getDayOfWeek) .filter(dayOfWeek -> DayOfWeek.SUNDAY.equals(dayOfWeek)) .orElse(DayOfWeek.MONDAY);

Прочие нюансы

Далее идут дополнительные советы и житейские мудрости, которые помогут вам еще лучше овладеть этим инструментом.

Когда стоит использовать Optional

Если открыть javadoc Optional , можно найти там прямой ответ на данный вопрос.

Optional в первую очередь предназначен для использования в качестве типа возвращаемого значения метода , когда существует явная необходимость представлять «отсутствие результата» и где использование null может вызвать ошибки.

Как НЕ стоит использовать Optional

А теперь разберемся основные ошибки при использовании Optional .

Как параметр метода

Не стоит использовать Optional , в качестве параметра метода. Если пользователь метода с параметром Optional не знает об этом, он может передать методу null вместо Optional.empty() . И обработка null приведет к исключению NullPointerException .

Как свойство класса

Также не используйте Optional для объявления свойств класса.

Во-первых, у вас могут возникнуть проблемы с такими популярными фремворками, как Spring Data/Hibernate. Hibernate не может замапить значения из БД на Optional напрямую , без кастомных конвертеров.

Хотя некоторые фреймворки, такие как Jackson отлично интегрируют Optional в свою экосиситему. Как раз с Jackson можно подумать об использовании Optional в качестве свойства класса, если создание объекта этого класса вы никика не контролируете. Например, при использовании Webhook.

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

В-третьих, использование таких полей будет неудобным Optional не имплементирует интерфейс Serializable . Проще говоря, любой объект, который содержит хотя бы одно Optional поле, нельзя сериализовать. Хотя с приходом микросервисов платформенная сериализация не является настолько важной, как раньше.

Решением в такой ситуации может быть использование Optional для геттеров класса. Однако у этого подхода есть один недостаток. Его нельзя полностью интегрировать с Lombok. Optional getters не подерживаются библиотекой и, судя по некоторым обсуждениям на Github, не будут.

Как обертка коллекции

Не оборачивайте коллекции в Optional . Любая коллекция является контейнером сама по себе. Чтобы вернуть пустую коллекцию, вместо null , можно воспользоваться следующими методами Collections.emptyList() , Collections.emptySet() и прочими.

Optional не должен равняться null

Присваивание null вместо объекта Optional разрушает саму концепцию его использования. Никто из пользователей вашего метода не будет проверять Optional на эквивалентность с null . Вместо присваивания null следует использовать Optional.empty() .

Примитивы и Optional

Для работы с обертками примитивов есть java.util.OptionalDouble , java.util.OptionalInt и java.util.OptionalLong , которые позволяют избегать лишних автоупаковок и распаковок. Однако не смотря на это, на практике используются они крайне редко.

Все эти классы похожи на Optional , но не имеют методов преобразования. В них доступны только: get , orElse , orElseGet , orElseThrow , ifPresent и isPresent .

Резюмирую

Класс Optional не решает проблему NullPointerException полностью, но при правильном применении позволяет снизить количество ошибок, сделать код более читабельным и компактным. Использование Optional не всегда уместно, но для возвращаемых значений из методов он подходит отлично.

Основные моменты, которые стоит запомнить

  • Если методу требуется вернуть null , верните Optional ;
  • Не используйте Optional в качестве параметра методов и как свойство класса;
  • Проверяйте Optional перед тем, как достать его содержимое;
  • Никогда так не делайте: optional.orElse(null) ;

Дополнительные ссылки по теме

Введение в Optional

В JDK 8 был введён новый утилитарный класс java.util.Optional, целями которого являются повышение null-safety и читаемости кода при работе со ссылками, значения которых могут быть null. Главная же цель Optional — замена null-значений, благодаря чему должна повышаться безопасность и читаемость кода.

Optional тесно связан с другими нововведениями JDK 8: Stream и лямбда-выражениями.

Пример использования

Допустим, у нас есть класс Employee, интерфейс EmployeeRepository для работы с источником данных и его реализация CachingEmployeeRepository, которая реализует кэширование и делегирует вызовы объекту другого реализующего класса.

В методе findOneById, идеальном для использования кэширования, общая последовательность действий выглядит следующим образом:

  • Попытаться получить объект Employee из кэша
  • Если кэш вернул null, то вызвать метод findOneById у объекта-делегата
  • Если делегат вернул не null, то сохранить значение в кэше
  • Вернуть значение или выкинуть исключение NoSuchElementException

Примерно так будет выглядеть реализация метода findOneById без использования Optional:

А так будет выглядеть реализация этого же метода уже с использованием Optional:

Обратите внимание, что в коде отсутствуют проверки на null, что достигнуто благодаря Optional. Но тут есть два неудобных момента:

  • Если предполагается несколько вариантов получения объекта, то цепочка действий превратится эдакую «лесенку»: мы создаём Optional, в методе orElseGet создаём альтернативный Optional и т.д.
  • Отсутствует метод, который бы принимал в качестве аргумента объект класса java.util.function.Consumer для работы со значением Optional, если оно представлено, аналогично Stream.peek из JDK или Mono.doOnNext из Reactor.

Первый неудобный момент решается использованием метода or вместо orElseGet, который был добавлен в JDK 9. С его применением, метод findOneById приобретёт следующий вид:

Пример OTN

В оригинальной статье на OTN, посвящённой Optional, в качестве примера приводилась следующая структура классов:

В результате чего абстрактная попытка получить версию USB установленной в компьютере звуковой карты будет выглядеть следующим образом:

Первый вариант может привести к NullPointerException, второй же исключает эту ситуацию, но требует большого количества кода для реализации достаточно простой задачи. Вариант с применением Optional будет выглядеть значительно проще:

Работа с Optional

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

Методы создания

Объект типа Optional может быть создан тремя методами:

  • Optional.of — для создания Optional из не-null значения. При передаче в качестве аргумента null будет выкинуто исключение NullPointerException.
  • Optional.ofNullable — для создания Optional из значения, которое может быть null. Если значение не null, то будет создан Optional со значением, иначе — пустой Optional.
  • Optional.empty — для создания пустого Optional.

Методы преобразования

Непустой Optional может быть преобразован в другой Optional, который может содержать другое значение, значение другого типа или и вовсе быть пустым. Поскольку методы filter, map, flatMap и orвозвращают объекты типа Optional, можно создавать цепочки преобразований практически любой длины.

filter() — фильтрация значения

Методом filter значение Optional может быть «отфильтровано». В качестве аргумента метод принимает объект типа java.util.function.Predicate. Если значение удовлетворяет условиям фильтрации, то возвращён будет тот же Optional, в противном случае — пустой Optional.

map() и flatMap() — преобразование значения

Методом map из текущего Optional можно получить новый объект Optional, содержащий другое значение. В качестве аргумента метод принимает объект типа java.util.function.Function. Если переданная функция возвращает null, то метод map вернёт пустой Optional.

Метод flatMap работает аналогично, но он декларирует, что переданная функция должна возвращать другой Optional, пустой или содержащий новое значение.

or()

В JDK 9 был добавлен метод or, позволяющий пустой Optional преобразовать в непустой. В качестве аргумента он принимает объект типа java.util.function.Supplier, метод get которого возвращает новый Optional и будет вызван только в том случае, если существующий Optional пустой.

Методы проверки

Единственным до JDK 11 методом проверки наличия значения был isPresent, который возвращает true, если объект Optional содержит значение, иначе — false.

В JDK 11 был добавлен метод-антипод isEmpty, возвращающий true, если значение отсутствует.

Методы получения значения

Для получения значения из Optional существует несколько методов, некоторые из которых предусматривают получение значений по умолчанию, если Optional не содержит никакого значения.

get()

Метод get — простейший способ получения значения объекта Optional. Если Optional непустой, то он вернёт значение, иначе выбросит исключение NoSuchElementException. Поэтому при обращении к этому методу нужно либо проверять наличие значения при помощи Optional.isPresent(), либо использовать try/catch.

orElse()

Метод orElse возвращает значение объекта Optional или значение, переданное ему в качестве аргумента, которое может быть null.

orElseGet()

Метод orElseGet, в отличии от orElse принимает в качестве аргумента объект класса java.util.function.Supplier, который вернёт «запасное» значение, если объект Optional пустой. Таким образом, в отличии от метода orElse, метод orElseGet создаёт «запасное» значение только по необходимости, что немного правильнее с точки зрения использования ресурсов, особенно, когда получение значения умолчанию требует выполнения каких-либо блокирующих операций, например, обращения к базе данных.

orElseThrow()

Метод orElseThrow возвращает значение объекта Optional или выбрасывает исключение, возвращаемое объектом типа java.util.function.Supplier переданным ему в качестве аргумента.
В JDK 10 был добавлен orElseThrow без аргументов, выбрасывающий NoSuchElementException.

Условные методы

Условные методы ifPresent и ifPresentOrElse позволяют выполнить необходимое действие над значением объекта Optional, если оно задано.

ifPresent()

Метод ifPresent принимает в качестве аргумента объект типа java.util.function.Consumer, метод accept которого принимает текущее значение объекта Optional в качестве аргумента. Метод accept будет вызван только в том случае, если Optional содержит значение.

ifPresentOrElse()

Метод ifPresentOrElse аналогичен методу ifPresent, он был добавлен в JDK 9 и в качестве второго аргумента принимает объект типа java.lang.Runnable, метод run которого будет вызван, если объект Optional не содержит значения.

Прочие методы

Optional реализует методы hashCode, equals и toString с использованием хранимого значения. Вызовы hashCode и equals делегируются объектам, использующимся в качестве значений Optional, а toString составляет строчное представление с использованием строчного представление объекта-значения.

Добавленный в JDK 9 метод stream возвращает объект типа java.util.stream.Stream, содержащий либо один элемент, либо пустой.

Заключение

Как бы то ни было, Optional не решает проблему NullPointerException полностью, но при правильном применении значительно снижает шансы возникновения этого исключения. При использовании Optional совместно с лямбда-выражениями код может выглядеть не совсем очевидно, что особенно хорошо видно по примеру с цепочкой из нескольких вызовов Optional.orElseGet(), но одновременно с этим код с использованием Optional в большинстве случаев будет выглядеть более логичным, чем с несколькими вложенными блоками if/else.

Класс Optional

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

Введен класс java.util.Optional начиная с версии Java 8.

Если кратко, то Optional либо хранит объект типа T, либо не хранит ничего (обертка над null). Далее мы приведем пример, показывающий, как плохо жить без Optional, и какие проблемы он решает.

Проблема с null

Давайте рассмотрим пример, демонстрирующий проблему. Допустим, у нас есть вложенная структура: Квартира (Apartment), Кухня (Kitchen), Холодильник (Fridge), Марка холодильника (brand). Марку холодильника мы получаем так:

String brand = apartment.getKitchen().getFridge().getBrand();

На первый взгляд все логично. Но проблема в том, что не всегда в квартире есть кухня (к примеру), а в кухне, в свою очередь, не всегда стоит холодильник.

Так что любой из этих геттеров может провалиться и завершить аварийно нашу программу.

Например, кухни нет, тогда getKitchen() возвращает null — это распространенная практика. Но тогда следующий за ним геттер getFridge() вызывается на объекте null, и программа завершается с NullPointerException.

Такие ситуации обходятся проверками:

String brand = "UNKNOWN"; if (apartment != null) < Kitchen kitchen = apartment.getKitchen(); if (kitchen != null) < Fridge fridge = kitchen.getFridge(); if (fridge != null) < brand = fridge.getBrand(); >> >

Но код получается длинный, некрасивый, плохочитаемый из-за вложенных проверок, одну из которых к тому же можно забыть сделать.

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

Kitchen kitchen;

возвращать из метода:

Optional kitchen;

Теперь пара слов об аналогах, которые вдохновили инженеров Oracle написать класс Optional.

Что есть аналогичное в других языках

Проблема хранения и возврата отсутствующего значения существует и в других языках. Она как-то решается. На момент создания класса Optional мы имеем следующее.

  • В Groove есть конструкция с оператором «?.» и с Elvis-оператором «?:»
String brand = apartment?.getKitchen()?.getFridge()?.getBrand() ?: "UNKNOWN";
  • В Haskell есть тип Maybe.
  • В Scala есть тип Option[T] — очень похоже на наш Optional

Optional в Java 8

Итак, что же у нас в Java 8? Вернемся к примеру.

С Optional класс Apartment будет такой:

import java.util.Optional; public class Apartment < private Kitchen kitchen; public OptionalgetKitchen() < return Optional.ofNullable(kitchen); >public void setKitchen(Kitchen kitchen) < this.kitchen = kitchen; >>

Класс Kitchen такой:

public class Kitchen < private Fridge fridge; public OptionalgetFridge() < return Optional.ofNullable(fridge); >public void setFridge(Fridge fridge) < this.fridge = fridge; >>
public class Fridge < private String brand; public String getBrand() < return brand; >public void setBrand(String brand) < this.brand = brand; >>

Глядя на эти классы, сразу понятно, что kitchen и fridge могут иметь, а могут не иметь значений (иначе говоря, быть null). Это упрощает чтение кода.

Кроме того, если объект имеет тип Optional, то вам автоматически придется учитываете возможность нуля и пропустить проверку на null будет сложнее.

В итоге с Optional получение марки будет выглядеть так:

String brand = apartment.flatMap(Apartment::getKitchen) .flatMap(Kitchen::getFridge) .map(Fridge::getBrand) .orElse("UNKNOWN");

Теперь о том, как создавать объект типа Optional (хотя мы уже создавали его в методах выше).

Создание Optional

Optional — это обертка для объекта типа T. Мы создаем Optional, оборачивая обычный объект. Есть три метода обернуть объект: обернуть можно null, ненулевой объект и есть метод для оборачивания любого (как нулевого, так и ненулевого) объекта.

Optional kitchen = Optional.empty();

Этот метод возвращает Optional, который оборачивает нулевую кухню. Если же мы знаем, что кухня ненулевая, обернуть ее можно с помощью метода Optional.of():

Optional kitchen = Optional.of(new Kitchen());

Обратите внимание, что если в этот метод передать null, будет выброшено исключение:

@Test(expected = NullPointerException.class) public void givenNull_whenOf_thenException()

Но обычно мы оборачиваем объект, про который неизвестно, null ли он, например, как в геттерах выше. Для этого используется метод Optional.ofNullable. Он не выбросит исключение, даже если передаваемый объект равен null:

Optional kitchen1 = Optional.ofNullable(new Kitchen()); Optional kitchen2 = Optional.ofNullable(null); assertThat(kitchen1.isPresent(), is(true)); assertThat(kitchen2.isPresent(), is(false));

Обратите внимание, мы использовали в тесте метод isPresent(), он возвращает true, если Optional содержит ненулевое значение.

Теперь рассмотрим некоторые типовые ситуации, когда Optional полезен.

Проверка на null — isPresent()

Часто в старом коде встречаются такие проверки:

Kitchen kitchen =. if (kitchen != null)

С новым классом вышеприведенный код выполняется так:

Optional maybeKitchen = Optional.ofNullable(new Kitchen()); maybeKitchen.ifPresent(System.out::println);

Значение или действие по умолчанию — orElse(), orElseThrow()

Для проверки и присвоения значения по умолчанию использовался тернарный оператор:

Kitchen kitchen = new Kitchen(); Kitchen anotherKitchen = kitchen != null ? kitchen : new Kitchen();

С Optional это делается так:

Optional maybeKitchen = Optional.ofNullable(new Kitchen()); Kitchen anotherKitchen = maybeKitchen.orElse(new Kitchen());

Допустим, надо извлечь и присвоить значение из Optional , если оно не null, либо выбросить исключение, если оно null.

Вот здесь происходит присваивание, поскольку maybeKitchen содержит не null:

Optional maybeKitchen = Optional.ofNullable(new Kitchen()); Kitchen kitchen = maybeKitchen.orElseThrow(IllegalArgumentException::new);

А здесь будет выброшено исключение:

Optional maybeKitchen = Optional.ofNullable(null); Kitchen kitchen = maybeKitchen.orElseThrow(IllegalArgumentException::new);

Сложная проверка — filter()

Ранее мы проверяли наличие значения с помощью ifPresent() и делали что-то. Допустим, надо выполнить более сложную проверку. Давайте напечатаем марку холодильника, причем только в том случае, если она равна «LG».

Fridge fridge=new Fridge(); fridge.setBrand(«LG»); if(fridge!=null && fridge.getBrand().equals(«LG»))

Optional maybeFridge = Optional.ofNullable(fridge); maybeFridge.filter(f->f.getBrand().equals("LG")) .ifPresent(f->System.out.println(f.getBrand()));

Метод filter() возвращает тот Optional, на котором вызван, если выполняется условие. Иначе он возвращает Optional.empty().

Извлечение и преобразование значений с помощью map()

Вышеприведенную задачу можно также решить с помощью метода map():

Optional brand = maybeFridge.map(Fridge::getBrand); brand.filter(b -> b.equals("LG")).ifPresent(b -> System.out.println(b));

В map() передается функция преобразования, которая применяется к значению в Optional, если оно не null. Иначе возвращается Optional.empty(). Исключений не выбрасывается, независимо от того, null ли холодильник или нет.

Во второй строке мы делаем фильтрацию и печатаем значение.

flatMap() для цепочки Optional

Вернемся к нашей задаче — получить по цепочке кухню, холодильник и марку. Марку из холодильника мы получили в последнем примере с помощью map(). При этом избавились от проблемы NullPointerException.

На первый взгляд кажется, что аналогично можно извлечь кухню и холодильник. Но это не совсем так. Геттеры кухни и холодильника возвращают не просто значение обычного типа, как getBrand(), а типы Optional . При вызове map() мы получим вложенный Optional:

Apartment apartment = new Apartment(); Optional maybeApartment = Optional.ofNullable(apartment); Optional> mbKitchen=maybeApartment.map(Apartment::getKitchen);

Чтобы убрать вложенный Optional, придуман метод flatMap(). С его помощью мы можем вызвать цепочку геттеров:

@Test public void whenFlatMap_thenOk() < Apartment apartment = new Apartment(); Kitchen kitchen=new Kitchen(); Fridge fridge = new Fridge(); fridge.setBrand("LG"); kitchen.setFridge(fridge); apartment.setKitchen(kitchen); OptionalmaybeApartment = Optional.ofNullable(apartment); // здесь цепочка String brand = maybeApartment.flatMap(Apartment::getKitchen) .flatMap(Kitchen::getFridge) .map(Fridge::getBrand) .orElse("UNKNOWN"); assertThat(brand, equalTo("LG")); >

На последнем звене цепочки мы вызываем map(), потому что getBrand() возвращает не Optional, а обычное значение — это было проделано уже в предыдущем разделе.

Для кухни же и квартиры мы используем flatMap().

Обратите внимание, что если нашу структуру значениями не заполнять, то никакого NullPointerException не будет (хотя в в старом коде исключение возникало). Здесь мы создаем Apartment, но никакого холодильника и кухни ей не назначаем, и все методы, тем не менее, вызываются безопасно:

@Test public void whenFlatMap_thenNoException() < Apartment apartment = new Apartment(); OptionalmaybeApartment = Optional.ofNullable(apartment); String brand = maybeApartment.flatMap(Apartment::getKitchen) .flatMap(Kitchen::getFridge) .map(Fridge::getBrand) .orElse("UNKNOWN"); assertThat(brand, equalTo("UNKNOWN"));

Итог

Мы научились пользоваться Optional; если методы преобразований не понятны, то почитайте про стримы, так как разработчики класса Optional их позаимствовали оттуда.

При чтении кода с Optional сразу понятно, где значения не обязательны, и код гораздо короче.

Код вышеприведенных примеров доступен на GitHub.

Автор sysout Опубликовано 23.08.2018 31.12.2020 Рубрики Core Java

Класс Optional: 2 комментария

Алексей :

А ничего, что flatmap принимает функциональный интерфейс function и передаваемая конструкция Apartment::getKitchen не совпадает по сигнатуре с методом apply интерфейса function?как ссылка на метод в таком случае передается?

sysout :

Все совпадает, Apartment::getKitchen равносильно а->a.getKitchen(), где а — текущий объект типа Apartment. таким образом в Function T — Apartment, R — Optional )
Просто есть разные типы method references, иногда левая часть лямбда выражения — вызывающий объект, а иногда аргумент (а иногда оба). У нас вызывающий объект в левой части (это unbound method reference). Еще пример unbound method reference String::toUpperCase. Подробнее см тут.
Если бы getKithen был статическим, то это был бы static method reference, и переносился бы аргумент.

Добавить комментарий Отменить ответ

Прошу прощения: на комментарии временно не отвечаю.

Руководство по Java 8 Optional

В этом учебном пособии мы рассмотрим класс Optional , который был представлен в Java 8.

Цель класса — предоставить решение на уровне типа для представления опциональных значений вместо null (нулевых) ссылок.

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

2. Создание объектов Optional

Существует несколько способов создания объектов Optional .

Чтобы создать пустой объект Optional , нужно просто использовать его статический метод empty() :

@Test public void whenCreatesEmptyOptional_thenCorrect() < Optionalempty = Optional.empty(); assertFalse(empty.isPresent()); >

Обратите внимание, что мы использовали метод isPresent() для проверки наличия значения внутри объекта Optional . Значение присутствует, только если мы создали Optional с non-null (ненулевым) значением. Мы рассмотрим метод isPresent() в следующем разделе.

Можно также создать объект Optional с помощью статического метода of() :

@Test public void givenNonNull_whenCreatesNonNullable_thenCorrect() < String name = "baeldung"; Optionalopt = Optional.of(name); assertTrue(opt.isPresent()); >

Однако аргумент, переданный в метод of() , не может быть null . В противном случае мы получим NullPointerException :

@Test(expected = NullPointerException.class) public void givenNull_whenThrowsErrorOnCreate_thenCorrect()

Но в случае, если мы предполагаем некоторые значения null , то можно использовать метод ofNullable() :

@Test public void givenNonNull_whenCreatesNullable_thenCorrect() < String name = "baeldung"; Optionalopt = Optional.ofNullable(name); assertTrue(opt.isPresent()); >

Таким образом, если мы передаем null ссылку, это не вызовет исключения, а вернет пустой объект Optional :

@Test public void givenNull_whenCreatesNullable_thenCorrect() < String name = null; Optionalopt = Optional.ofNullable(name); assertFalse(opt.isPresent()); >

3. Проверка наличия значения: isPresent() и isEmpty()

Когда у нас есть объект Optional , возвращенный из метода или созданный нами, мы можем проверить, есть ли в нем значение или нет, с помощью метода isPresent() :

@Test public void givenOptional_whenIsPresentWorks_thenCorrect() < Optionalopt = Optional.of("Baeldung"); assertTrue(opt.isPresent()); opt = Optional.ofNullable(null); assertFalse(opt.isPresent()); >

Этот метод возвращает true , если обернутое значение не является null .

Также, начиная с Java 11, мы можем сделать обратное с помощью метода isEmpty :

@Test public void givenAnEmptyOptional_thenIsEmptyBehavesAsExpected() < Optionalopt = Optional.of("Baeldung"); assertFalse(opt.isEmpty()); opt = Optional.ofNullable(null); assertTrue(opt.isEmpty()); >

4. Условное действие с помощью ifPresent()

Метод ifPresent() позволяет нам запустить некоторый код для обернутого значения, если выяснится, что оно не является null . До метода Optional мы бы сделали следующее:

if(name != null)

Этот код проверяет, является ли переменная name null или нет, прежде чем приступить к выполнению какого-либо кода над ней. Такой подход занимает много времени, и это не единственная проблема — он также склонен к ошибкам.

В самом деле, где гарантия, что после печати этой переменной мы не воспользуемся ею снова, а потом забудем выполнить проверку на null ?

Это может привести к NullPointerException во время выполнения программы, если в этот код попадет значение null . Когда программа терпит неудачу из-за проблем с вводом, это часто является результатом плохой практики программирования.

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

Теперь давайте посмотрим, каким образом приведенный выше код может быть рефакторизован в Java 8.

В типичном стиле функционального программирования мы можем выполнить действие над объектом, который действительно присутствует:

@Test public void givenOptional_whenIfPresentWorks_thenCorrect() < Optionalopt = Optional.of("baeldung"); opt.ifPresent(name -> System.out.println(name.length())); >

5. Значение по умолчанию с помощью orElse()

Метод orElse() используется для получения значения, обернутого внутри экземпляра Optional . Он принимает один параметр, который выступает в качестве значения по умолчанию. Метод orElse() возвращает обернутое значение, если оно присутствует, либо его аргумент в противном случае:

@Test public void whenOrElseWorks_thenCorrect()

6. Значение по умолчанию с помощью orElseGet()

Метод orElseGet() аналогичен методу orElse() . Однако вместо того, чтобы принимать значение для возврата, если Optional значение отсутствует, он принимает функциональный интерфейс поставщика, который вызван и возвращает значение вызова:

@Test public void whenOrElseGetWorks_thenCorrect() < String nullName = null; String name = Optional.ofNullable(nullName).orElseGet(() ->"john"); assertEquals("john", name); >

7. Разница между orElse и orElseGet()

Многим программистам, которые только начинают работать с Optional или Java 8, разница между orElse() и orElseGet() непонятна. На самом деле, создается впечатление, что эти два метода перекрывают друг друга по функциональности.

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

Давайте создадим в тестовом классе метод getMyDefault() , который не принимает никаких аргументов и возвращает значение по умолчанию:

public String getMyDefault()

Давайте рассмотрим два теста и понаблюдаем за их побочными эффектами, чтобы определить, где orElse() и orElseGet() пересекаются, а где отличаются:

@Test public void whenOrElseGetAndOrElseOverlap_thenCorrect()

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

Побочный эффект таков:

Getting default value. Getting default value. 

В каждом случае вызывается метод getMyDefault() . Выходит, что если обернутое значение отсутствует, то и orElse() , и orElseGet() работают одинаково.

Теперь давайте проведем еще один тест, в котором значение присутствует, и в идеале значение по умолчанию даже не должно создаваться:

@Test public void whenOrElseGetAndOrElseDiffer_thenCorrect()

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

Теперь давайте посмотрим на побочный эффект от выполнения этого кода:

Using orElseGet: Using orElse: Getting default value. 

Обратите внимание, что при использовании orElseGet() для извлечения обернутого значения метод getMyDefault() даже не вызывается, поскольку содержащееся значение присутствует.

Однако при использовании orElse() , независимо от того, есть ли обернутое значение или нет, создается объект по умолчанию. Таким образом, в данном случае мы просто создали один лишний объект, который никогда не используется.

В этом простом примере создание объекта по умолчанию не требует значительных затрат, поскольку JVM знает, как с ним обращаться. Однако когда такой метод, как getMyDefault() , должен выполнить вызов веб-сервиса или даже запрос к базе данных, затраты становятся весьма очевидными.

8. Исключения с помощью orElseThrow()

Метод orElseThrow() следует из orElse() и orElseGet() и добавляет новый подход к обработке отсутствующего значения.

Вместо того чтобы возвращать значение по умолчанию, когда обернутое значение отсутствует, он выбрасывает исключение:

@Test(expected = IllegalArgumentException.class) public void whenOrElseThrowWorks_thenCorrect()

Здесь пригодятся ссылки на методы в Java 8, чтобы передать в конструктор исключения.

Java 10 представила упрощенную версию метода orElseThrow() без аргументов. В случае пустого Optional он выбрасывает исключение NoSuchElementException :

@Test(expected = NoSuchElementException.class) public void whenNoArgOrElseThrowWorks_thenCorrect()

9. Возвращение значения с помощью get()

Последний способ получения обернутого значения — метод get() :

@Test public void givenOptional_whenGetsValue_thenCorrect() < Optionalopt = Optional.of("baeldung"); String name = opt.get(); assertEquals("baeldung", name); >

Однако, в отличие от предыдущих трех подходов, get() может вернуть значение, только если обернутый объект не является null ; в противном случае он выбрасывает исключение нет такого элемента:

@Test(expected = NoSuchElementException.class) public void givenOptionalWithNull_whenGetThrowsException_thenCorrect() < Optionalopt = Optional.ofNullable(null); String name = opt.get(); >

Это основной недостаток метода get() . В идеале Optional должен помочь нам избежать таких непредвиденных исключений. Поэтому данный подход противоречит целям Optional и, вероятно, будет упразднен в одном из будущих выпусков.

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

10. Условный возврат с помощью filter()

Мы можем запустить встроенный тест на нашем обернутом значении с помощью метода filter . Он принимает предикат в качестве аргумента и возвращает объект Optional . Если обернутое значение проходит проверку предикатом, то Optional возвращается как есть.

Однако если предикат вернет false , то будет возвращен пустой Optional :

@Test public void whenOptionalFilterWorks_thenCorrect() < Integer year = 2016; OptionalyearOptional = Optional.of(year); boolean is2016 = yearOptional.filter(y -> y == 2016).isPresent(); assertTrue(is2016); boolean is2017 = yearOptional.filter(y -> y == 2017).isPresent(); assertFalse(is2017); >

Метод filter обычно используется таким образом для отклонения обернутых значений на основе предопределенного правила. Мы могли бы использовать его для отбраковки неправильного формата электронной почты или недостаточно надежного пароля.

Давайте рассмотрим другой показательный пример. Допустим, мы хотим купить модем, и нас интересует только его цена.

С определенного сайта нам приходят push-уведомления о ценах на модемы, и они сохраняются в объектах:

public class Modem < private Double price; public Modem(Double price) < this.price = price; >// standard getters and setters >

Затем мы передаем эти объекты некоторому коду, единственная цель которого — проверить, находится ли цена модема в пределах нашего бюджета.

Теперь давайте посмотрим на код без Optional :

public boolean priceIsInRange1(Modem modem) < boolean isInRange = false; if (modem != null && modem.getPrice() != null && (modem.getPrice() >= 10 && modem.getPrice() return isInRange; >

Обратите внимание на то, как много кода нам приходится писать для достижения этой цели, особенно в условии if . Единственная часть условия if , которая критична для приложения, — это последняя проверка диапазона цен; остальные проверки носят вспомогательный характер:

@Test public void whenFiltersWithoutOptional_thenCorrect()

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

Теперь давайте рассмотрим вариант с Optional#filter :

public boolean priceIsInRange2(Modem modem2) < return Optional.ofNullable(modem2) .map(Modem::getPrice) .filter(p ->p >= 10) .filter(p -> p

Вызов map используется для трансформации одного значения в другое. Следует помнить, что эта операция не изменяет исходное значение.

В нашем случае мы получаем объект цены из класса Model . Метод map() будет подробно рассмотрен в следующем разделе.

Во-первых, если в этот метод передается объект null , то никаких проблем не предвидится.

Во-вторых, единственная логика, которую мы прописываем внутри его тела, это именно то, что описывает название метода — проверка ценового диапазона. Об остальном позаботится Optional :

@Test public void whenFiltersWithOptional_thenCorrect()

Предыдущий подход обещает проверить диапазон цен, но при этом должен сделать нечто большее, чтобы защититься от присущей ему хрупкости. Поэтому мы можем использовать метод filter , чтобы заменить ненужные операторы if и отбросить нежелательные значения.

11. Трансформация значения с помощью map()

В предыдущем разделе мы рассмотрели, как отклонить или принять значение на основе фильтра.

Мы можем использовать аналогичный синтаксис для преобразования значения Optional с помощью метода map() :

@Test public void givenOptional_whenMapWorks_thenCorrect() < ListcompanyNames = Arrays.asList( "paypal", "oracle", "", "microsoft", "", "apple"); Optional listOptional = Optional.of(companyNames); int size = listOptional .map(List::size) .orElse(0); assertEquals(6, size); >

В этом примере мы оборачиваем список строк внутри объекта Optional и используем его метод map для выполнения действия над содержащимся в нем списком. Оно заключается в получении размера списка.

Метод map возвращает результат вычислений, завернутый внутрь Optional . Затем мы должны вызвать соответствующий метод для возвращенного Optional , чтобы получить это значение.

Обратите внимание, что метод filter просто выполняет проверку значения и возвращает Optional , описывающий это значение, только если оно соответствует заданному предикату. В противном случае возвращается пустой Optional . Метод map берет существующее значение, выполняет вычисления, используя его, и возвращает результат вычислений, обернутый в объект Optional :

@Test public void givenOptional_whenMapWorks_thenCorrect2() < String name = "baeldung"; OptionalnameOptional = Optional.of(name); int len = nameOptional .map(String::length) .orElse(0); assertEquals(8, len); >

Мы можем соединить map и filter вместе, чтобы сделать что-то более мощное.

Допустим, нам нужно проверить правильность пароля, введенного пользователем. Можно очистить пароль с помощью трансформации map и проверить его правильность с помощью filter :

@Test public void givenOptional_whenMapWorksWithFilter_thenCorrect() < String password = " password "; OptionalpassOpt = Optional.of(password); boolean correctPassword = passOpt.filter( pass -> pass.equals("password")).isPresent(); assertFalse(correctPassword); correctPassword = passOpt .map(String::trim) .filter(pass -> pass.equals("password")) .isPresent(); assertTrue(correctPassword); >

Как мы видим, без предварительной очистки ввода он будет отфильтрован. Однако пользователи могут считать само собой разумеющимся, что начальные и конечные пробелы — это всё входные данные. Поэтому мы преобразуем измененный пароль в очищенный с помощью map , прежде чем отфильтровать неправильные пароли.

12. Трансформация значения с помощью flatMap()

Подобно методу map() , у нас также есть метод flatMap() в качестве альтернативы для трансформации значений. Разница в том, что map преобразует значения только тогда, когда они извлечены (развернуты), в то время как flatMap берет обернутое значение и разворачивает его перед трансформацией.

Ранее мы создавали простые объекты String и Integer для обертывания в экземпляр Optional . Теперь, зачастую, мы будем получать эти объекты от асессора сложного объекта.

Чтобы лучше понять разницу, давайте рассмотрим объект Person , который принимает данные человека, такие как имя, возраст и пароль:

public class Person < private String name; private int age; private String password; public OptionalgetName() < return Optional.ofNullable(name); >public Optional getAge() < return Optional.ofNullable(age); >public Optional getPassword() < return Optional.ofNullable(password); >// normal constructors and setters >

Обычно мы создаем такой объект и оборачиваем его в Optional , как мы это делали со String.

В качестве альтернативы он может быть возвращен нам другим вызовом метода:

Person person = new Person("john", 26); Optional personOptional = Optional.of(person);

Обратите внимание, что когда мы обернем объект Person , он будет содержать вложенные экземпляры Optional :

@Test public void givenOptional_whenFlatMapWorks_thenCorrect2() < Person person = new Person("john", 26); OptionalpersonOptional = Optional.of(person); Optional> nameOptionalWrapper = personOptional.map(Person::getName); Optional nameOptional = nameOptionalWrapper.orElseThrow(IllegalArgumentException::new); String name1 = nameOptional.orElse(""); assertEquals("john", name1); String name = personOptional .flatMap(Person::getName) .orElse(""); assertEquals("john", name); >

Здесь мы пытаемся извлечь атрибут name объекта Person , чтобы выполнить утверждение.

Обратите внимание, как мы достигаем этого с помощью метода map() в третьем операторе, а затем заметьте, как мы делаем то же самое с помощью метода flatMap() после этого.

Ссылка на метод Person::getName похожа на вызов String::trim , который мы использовали в предыдущем разделе для очистки пароля.

Единственное отличие заключается в том, что getName() возвращает Optional , а не String, как операция trim() . Это, в сочетании с тем, что трансформация map оборачивает результат в объект Optional , и приводит к появлению вложенного Optional .

Поэтому во время применения метода map() необходимо добавить дополнительный вызов для получения значения перед использованием трансформированного значения. Таким образом, обертка Optional будет удалена. Эта операция выполняется неявно при использовании flatMap .

13. Цепочка Optional в Java 8

Иногда нам может понадобиться получить первый непустой объект Optional из нескольких Optional . В таких случаях было бы очень удобно использовать метод типа orElseOptional() . К сожалению, в Java 8 такая операция напрямую не поддерживается.

Давайте сначала представим несколько методов, которые мы будем использовать на протяжении всего этого раздела:

private Optional getEmpty() < return Optional.empty(); >private Optional getHello() < return Optional.of("hello"); >private Optional getBye() < return Optional.of("bye"); >private Optional createOptional(String input) < if (input == null || "".equals(input) || "empty".equals(input)) < return Optional.empty(); >return Optional.of(input); >

Для того чтобы выстроить цепочку из нескольких объектов Optional и получить первый непустой объект в Java 8, мы можем использовать Stream API:

@Test public void givenThreeOptionals_whenChaining_thenFirstNonEmptyIsReturned() < Optionalfound = Stream.of(getEmpty(), getHello(), getBye()) .filter(Optional::isPresent) .map(Optional::get) .findFirst(); assertEquals(getHello(), found); >

Недостатком этого подхода является то, что все наши методы get выполняются всегда, независимо от того, где в Stream появляется непустой Optional .

Если мы хотим лениво оценить методы, переданные в Stream.of() , нам нужно использовать ссылку на метод и интерфейс Supplier :

@Test public void givenThreeOptionals_whenChaining_thenFirstNonEmptyIsReturnedAndRestNotEvaluated() < Optionalfound = Stream.>of(this::getEmpty, this::getHello, this::getBye) .map(Supplier::get) .filter(Optional::isPresent) .map(Optional::get) .findFirst(); assertEquals(getHello(), found); >

В случае если нам нужно использовать методы, принимающие аргументы, мы должны прибегнуть к лямбда-выражениям:

@Test public void givenTwoOptionalsReturnedByOneArgMethod_whenChaining_thenFirstNonEmptyIsReturned() < Optionalfound = Stream.>of( () -> createOptional("empty"), () -> createOptional("hello") ) .map(Supplier::get) .filter(Optional::isPresent) .map(Optional::get) .findFirst(); assertEquals(createOptional("hello"), found); >

Часто мы хотим вернуть значение по умолчанию в случае, если все цепочки Optional пусты. Мы можем сделать это, просто добавив вызов orElse() или orElseGet() :

@Test public void givenTwoEmptyOptionals_whenChaining_thenDefaultIsReturned() < String found = Stream.>>of( () -> createOptional("empty"), () -> createOptional("empty") ) .map(Supplier::get) .filter(Optional::isPresent) .map(Optional::get) .findFirst() .orElseGet(() -> "default"); assertEquals("default", found); >

14. Optional API в JDK 9

Релиз Java 9 добавил еще больше новых методов в Optional API:

  • метод or() для предоставления поставщика, который создает альтернативный Optional
  • ifPresentOrElse() метод, позволяющий выполнить действие, если Optional присутствует, или другое действие, если нет
  • метод stream() для преобразования Optional в Stream .

Вот полный текст статьи для дальнейшего чтения.

15. Неправильное использование Optional

Наконец, давайте рассмотрим привлекательный, но опасный способ использования Optional : передача параметра Optional методу.

Представьте, что у нас есть список Person , и мы хотим, чтобы метод искал в этом списке людей с заданным именем. Кроме того, желательно, чтобы этот метод находил записи, имеющие по крайней мере определенный возраст, если он указан.

Поскольку этот параметр является опциональным, мы пришли к такому методу:

public static List search(List people, String name, Optional age) < // Null checks for people and name return people.stream() .filter(p ->p.getName().equals(name)) .filter(p -> p.getAge().get() >= age.orElse(0)) .collect(Collectors.toList()); >

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

someObject.search(people, "Peter", null);

Теперь разработчик выполняет свой код и получает NullPointerException . Мы вынуждены осуществлять проверку на null нашего опционального параметра, что противоречит нашей первоначальной цели избежать подобной ситуации.

Вот некоторые возможности, которые можно было бы сделать, чтобы справиться с этим лучше:

public static List search(List people, String name, Integer age) < // Null checks for people and name final Integer ageFilter = age != null ? age : 0; return people.stream() .filter(p ->p.getName().equals(name)) .filter(p -> p.getAge().get() >= ageFilter) .collect(Collectors.toList()); >

Здесь параметр по-прежнему является опциональным, но мы обрабатываем его только в одной проверке.

Другим возможным вариантом является создание двух перегруженных методов:

public static List search(List people, String name) < return doSearch(people, name, 0); >public static List search(List people, String name, int age) < return doSearch(people, name, age); >private static List doSearch(List people, String name, int age) < // Null checks for people and name return people.stream() .filter(p ->p.getName().equals(name)) .filter(p -> p.getAge().get().intValue() >= age) .collect(Collectors.toList()); >

Таким образом, мы предлагаем четкий API с двумя методами, делающими разные вещи (хотя они имеют общую имплементацию).

Итак, существуют решения, позволяющие избежать использования Optional в качестве параметров метода. Намерением Java было при выпуске Optional использовать его в качестве возвращаемого типа, таким образом указывая, что метод может вернуть пустое значение. На самом деле, практика использования Optional в качестве параметра метода даже не рекомендуется некоторыми инспекторами кода.

16. Optional и сериализация

Как уже говорилось выше, Optional предназначен для использования в качестве возвращаемого типа. Пытаться использовать его в качестве типа поля не рекомендуется.

Кроме того, использование Optional в сериализуемом классе приведет к возникновению исключения NotSerializableException . В нашей статье Java Optional как возвращаемый тип вопросы сериализации рассматриваются подробнее .

А в статье Использование Optional в Jackson мы объясняем, что происходит при сериализации полей Optional , и также приводим несколько обходных путей для достижения желаемых результатов.

17. Заключение

В этой статье мы охватили большинство важнейших фич класса Optional в Java 8.

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

Также узнали, как получить значение Optional или значение по умолчанию, если оно пустое, с помощью методов get() , orElse() и orElseGet() (и заметили важную разницу между двумя последними).

Затем рассмотрели, как трансформировать или фильтровать наши Optional с помощью map() , flatMap() и filter() . Обсудили, что предлагает текучий API Optional , позволяющий легко соединять различные методы в цепочку.

Наконец, увидели, почему использование Optional в качестве параметров метода является плохой идеей и как этого избежать.

Исходный код всех примеров, приведенных в статье, доступен на GitHub.

Приглашаем все желающих на открытое занятие «Объектно-ориентированное и функциональное программирование», на котором разберем отличия между этими двумя подходами на максимально простом уровне. Примеры рассмотрим на языке Java. Во второй части занятия вас ждет подробное описание особенностей специализации Java-разработчик. Регистрация — по ссылке.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *