Коротко: Optional это контейнер, который либо содержит значение, либо пуст, и тем самым честно сообщает вызывающему коду, что результата может не быть. Документация Oracle уточняет назначение: Optional задуман прежде всего как тип возвращаемого значения метода там, где нужно выразить «результата нет», а null легко приведет к ошибке. Сам по себе он не убирает NullPointerException, но заставляет подумать о пустом случае на этапе написания кода. Благодаря этой статье вы сможете лучше понять предназначение Optional при работе с кодом Java. Когда я впервые начал работать с Java-кодом, мне часто советовали использовать Optional. Но я в то время плохо понимал, почему использование Optional лучше, чем реализации обработки для значений null. В этой статье я хочу поделиться с вами тем, зачем, по моему мнению, нам всем стоит больше использовать Optional, и как избежать чрезмерной “оптионализации” кода, которое вредит его качеству.

Кратко

  • Optional создают тремя способами: of() для заведомо непустого значения, ofNullable() для того, что может быть null, и empty() для пустого.
  • Вызов get() у пустого Optional бросает NoSuchElementException, поэтому с Java 10 вместо него пишут orElseThrow().
  • orElse() вычисляет запасное значение всегда, даже когда оно не понадобится. Если там вызов метода, берите orElseGet().
  • map() работает по цепочке и сам пропускает пустые значения, поэтому проверки isPresent() чаще всего лишние.
  • Документация прямо говорит: Optional это тип для возвращаемого значения, а не для полей и аргументов методов.
  • Переменная типа Optional никогда не должна сама быть null.

Что такое Optional?

Параметр Optional используется для переноса объектов и обеспечения обработки ссылок на null с помощью различных API-интерфейсов. Давайте посмотрим на отрывок кода:

Coffee coffee = new Coffee();
Integer quantity = coffee.getSugar().getQuantity();
У нас есть экземпляр Coffee, в котором мы получаем некоторое количество sugar из экземпляра объекта Sugar. Если мы предположим, что значение количества никогда не устанавливалось в конструкторе Coffee, то coffee.getSugar().getQuantity() вернет исключение NullPointerException. Конечно, мы всегда можем использовать старые добрые проверки null, чтобы исправить проблему.

Coffee coffee = new Coffee();
Integer quantity = 0;
if (coffee.getSugar() != null) {
  quantity = coffee.getSugar().getQuantity();
}
Теперь вроде бы все нормально. Но при написании Java-кода нам лучше избегать реализации проверок null. Давайте посмотрим, как это можно сделать, используя Optional.

Как создать Optional

Существует три способа создания объектов Optional:
  • of(T value): создание экземпляра Optional ненулевого (non-null) объекта. Имейте в виду, что использование of() для ссылки на объект null приведет к появлению NullPointerException.

  • ofNullable(T value): создание значения Optional для объекта, который может быть нулевым (null).

  • empty(): создание экземпляра Optional, который представляет ссылку на null.


// пример использования Optional.of(T Value)
String name = "foo";
Optional<String> stringExample = Optional.of(name);
// пример использования Optional.ofNullable(T Value)
Integer age = null;
Optional<Integer> integerExample = Optional.ofNullable(age);
// пример использования Optional.empty()
Optional<Object> emptyExample = Optional.empty();
Итак, у вас есть объект Optional. А теперь давайте познакомимся с двумя основными методами для Optional:
  • isPresent(): этот метод сообщает вам, содержит ли объект Optional ненулевое (non-null) значение.

  • get(): извлекает значение для Optional с текущим значением. Имейте в виду, что вызов get() для пустого Optional приведет к NoSuchElementException, а не к NullPointerException: так написано в документации класса. С Java 10 у этого метода есть более выразительный двойник orElseThrow(), который делает ровно то же самое, и в новом коде обычно пишут именно его.

Учтите, что если вы используете только get() и isPresent() при работе с Optional, вы многое упускаете! Чтобы это понять, давайте сейчас перепишем приведенный выше пример с Optional.

Улучшение проверки Null с помощью Optional

Итак, как нам улучшить приведенный выше код? С Optional мы можем понять наличие объекта с помощью isPresent() и получить его с помощью get(). Начнем с упаковки результата coffee.getSugar() с Optional и использованием метода isPresent(). Это поможет нам определить, возвращает ли getSugar() значение null.

Coffee coffee = new Coffee();
Optional<Sugar> sugar = Optional.ofNullable(coffee.getSugar());
int quantity = 0;
if (sugar.isPresent()) {
  quantity = sugar.get().getQuantity();
}
Глядя на этот пример, упаковка результата coffee.getSugar() в Optional, похоже, не прибавляет никакой ценности, а скорее добавляет хлопот. Мы можем улучшить результат, используя то, что я считаю своими любимыми функциями из класса Optional:
  • map(Function<? super T,? extends U> mapper): преобразует значение, содержащееся в Optional, с предоставленной функцией. Если параметр Optional пуст, то map() вернет Optional.empty().

  • orElse(T other) это “специальная” версия метода get(). Она может получить значение, содержащееся в Optional. Однако в случае пустого Optional это вернет значение, переданное в метод orElse().

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

Coffee coffee = new Coffee();

Integer quantity = Optional.ofNullable(coffee.getSugar())
    .map(it -> it.getQuantity())
    .orElse(0);
Это реально круто, по крайней мере я так думаю. Теперь, если в случае пустого значения мы не хотим возвращать значение по умолчанию, то нам нужно создать какое-то исключение. orElseThrow(Supplier<? extends X> exceptionSupplier) возвращает значение, содержащееся в параметрах Optional, или выдает исключение в случае пустого Optional.

Coffee coffee = new Coffee();

Integer quantity = Optional.ofNullable(coffee.getSugar())
  .map(it -> it.getQuantity())
  .orElseThrow(IllegalArgumentException::new);
Как видите, Optional дает несколько преимуществ:
  • абстрагирует проверки null
  • предоставляет API для обработки объектов null
  • позволяет декларативному подходу выражать то, что достигается
Однако есть один метод с опасно неожиданным поведением.

Познакомьтесь с методом orElse

Согласно документации Oracle:

public T orElse(T other)
Верните значение, если оно есть, в противном случае верните другое. Теперь мы можем добавить вызов метода в качестве параметра orElse, который будет запущен, если параметр Optional пуст, верно? Да, это правильно, но что, если я скажу вам, что оно будет работать в любом случае, независимо от наличия значения в Optional или нет. Давайте проверим:

@Test  
public void orElseTest()  
{  
    String result = Optional.of("hello").orElse(someMethod());  
    assertThat(result).isEqualTo("hello");  
}  
private String someMethod()  
{  
    System.out.println("I am running !!");  
    return "hola";  
}
Тест прошел успешно, но можно заметить, что на консоли распечатана строка “I am running”.

Почему так происходит?

Java запускает метод, чтобы предоставить значение, которое будет возвращено в случае Else.

Так что будьте осторожны!

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

Что же делать?

Вы можете использовать метод orElseGet, который принимает метод поставщика (supplier method) для выполнения, если существует Optional.

Как стать эффективным с Optional

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

Optional<Coffee> findByName(String name) {
   ...
}
Иногда в этом нет необходимости. Например, если у меня есть метод, возвращающий int, например getQuantity() в классе Sugar, тогда метод может вернуть 0, если результат равен null, чтобы представить “no quantity” (нет количества). Теперь, зная это, мы можем подумать, что параметр Sugar в классе Coffee может быть представлен как Optional. На первый взгляд это кажется хорошей идеей, поскольку теоретически сахар не обязательно должен присутствовать в кофе. Однако именно здесь я хотел бы остановиться на том, когда не следует использовать Optional. Мы должны избегать использования Optional в следующих сценариях:
  • В качестве типов параметров для POJO, таких как объекты DTO. POJO это обычный Java-класс с полями, геттерами и сеттерами, не привязанный ни к какому фреймворку, а DTO это его частный случай, объект для передачи данных между слоями приложения. Optional не сериализуемы, поэтому их использование в POJO лишает объект возможности сериализации.

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

  • Для представления объекта Collection, который отсутствует. Коллекции могут быть пустыми, поэтому пустая Collection, как пустой Set или List, должна использоваться для представления Collection без значений.

Как достать значение из Optional

Способов больше, чем get(), и выбор между ними и есть главное умение при работе с Optional:
Метод Что вернет Когда брать
get() значение или NoSuchElementException лучше не использовать, есть orElseThrow()
orElseThrow() значение или NoSuchElementException когда пустое значение это ошибка (с Java 10)
orElseThrow(Supplier) значение или ваше исключение когда нужно свое исключение с сообщением
orElse(value) значение или запасное запасное значение уже готово: константа, ноль, пустая строка
orElseGet(Supplier) значение или результат функции запасное значение надо вычислять: запрос, расчет, создание объекта
ifPresent(Consumer) ничего не возвращает, выполняет действие надо что-то сделать только при наличии
ifPresentOrElse(Consumer, Runnable) ничего не возвращает, выполняет одно из двух действий разные действия для полного и пустого (с Java 9)
Отдельно стоит запомнить isEmpty(): он появился в Java 11 и читается лучше, чем !isPresent().

Заключение

Optional стал мощным дополнением к библиотеке Java. Он дает способ обработки объектов, которые могут не существовать. Поэтому его следует учитывать при разработке методов, но не попадая в ловушку злоупотребления. Да, вы можете написать отличный код, который реализует проверки null и обработку null, но сообщество Java предпочитает использовать Optional. Он эффективно сообщает, как обрабатывать отсутствующие значения, его намного легче читать, чем беспорядочные проверки Null, и в долгосрочной перспективе это приводит к меньшему количеству ошибок в коде.

Вопросы и ответы

Что такое Optional в Java и зачем он нужен?

Это контейнер, который либо хранит значение, либо пуст. Нужен он для честного ответа вызывающему коду: результата может не быть. По документации Oracle это прежде всего тип возвращаемого значения метода там, где нужно выразить "результата нет" и где null легко приведет к ошибке.

Чем orElse отличается от orElseGet?

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

Почему не стоит вызывать get()?

Потому что на пустом Optional он бросает NoSuchElementException, и вы возвращаетесь к той же проблеме, от которой уходили с null, только исключение теперь другое. С Java 10 есть orElseThrow() без аргументов: он делает то же самое, но по имени видно, что метод может бросить исключение. А чаще всего вместо извлечения значения достаточно map и orElse.

Можно ли использовать Optional в полях класса и в аргументах методов?

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

Как проверить, что Optional пуст?

С Java 11 для этого есть метод isEmpty(), он читается лучше, чем !isPresent(). Но в большинстве случаев явная проверка вообще не нужна: цепочка map плюс orElse или orElseGet описывает оба случая сразу и короче.

Что вернет map(), если Optional пуст?

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