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

Что такое 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 приведет к NullPointerException.

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

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

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

Coffee coffee = new Coffee();
Optional<String> sugar = Optional.ofNullable(coffee.getSugar());
int quantity = 0;
if (sugar.isPresent()) {
  Sugar sugar = sugar.get();
  int quantity = sugar.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. Optional не сериализуемы, поэтому их использование в POJO лишает объект возможности сериализации.

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

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

Заключение

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