Класс Optional появился в Java 8. Задачей этого класса является предоставление решений на уровне типа-обертки для обеспечения удобства обработки возможных null-значений.
Звучит не слишком просто. Наглядно это можно представить так:
![Внутри объект может быть, а может и не быть Внутри объект может быть, а может и не быть](https://habrastorage.org/getpro/habr/upload_files/c5d/ee4/9b4/c5dee49b472d3a57d67af17946f1fc07.png)
Хорошее представление класса Optional есть в статье на сайте Oracle.
Создание объекта Optional
Существует несколько способов. Чтобы создать пустой объект можно использовать статический метод empty()
:
@Test
public void whenCreatesEmptyOptional_thenCorrect() {
Optional<String> empty = Optional.empty();
assertFalse(empty.isPresent());
}
Метод isPresent()
проверяет наличие значения внутри объекта Optional. Чуть позже мы вернемся к этому методу.
Также объект Optional можно создать с помощью статического метода of()
:
@Test
public void givenNonNull_whenCreatesNonNullable_thenCorrect() {
String name = "Wine";
Optional<String> opt = Optional.of(name);
assertTrue(opt.isPresent());
}
В этом варианте объект, который мы передаем в метод of()
, должен быть не null
. Иначе получим NullPointerException
:
@Test(expected = NullPointerException.class)
public void givenNull_whenThrowsErrorOnCreate_thenCorrect() {
String name = null;
Optional.of(name);
}
Если мы предполагаем, что передаваемое значение может быть null
, то используем другой метод - ofNullable()
:
@Test
public void givenNonNull_whenCreatesNullable_thenCorrect() {
String name = "One more wine";
Optional<String> opt = Optional.ofNullable(name);
assertTrue(opt.isPresent());
}
Если мы передадим в этот метод null
, то вместо NPE получим просто пустой объект Optional
:
@Test
public void givenNull_whenCreatesNullable_thenCorrect() {
String name = null;
Optional<String> opt = Optional.ofNullable(name);
assertFalse(opt.isPresent());
}
Проверка наличия значений: isPresent() и isEmpty()
Когда у нас имеется объект Optional
, который мы получаем из метода или создаем сами, с помощью метода isPresent()
можно проверить есть ли что-то внутри:
@Test
public void givenOptional_whenIsPresentWorks_thenCorrect() {
Optional<String> opt = Optional.of("Rioja wine region");
assertTrue(opt.isPresent());
opt = Optional.ofNullable(null);
assertFalse(opt.isPresent());
}
Метод возвращает true
, если оборачиваемое значение не null
. С Java 11 появился метод isEmpty()
, который делает ровно противоположное:
@Test
public void givenAnEmptyOptional_thenIsEmptyBehavesAsExpected() {
Optional<String> opt = Optional.of("Rioja 11");
assertFalse(opt.isEmpty());
opt = Optional.ofNullable(null);
assertTrue(opt.isEmpty());
}
Метод ifPresent()
Метод ifPresent()
позволяет нам выполнить код над объектом внутри Optional
, если он не null
. Раньше код был такой:
if(name != null) {
System.out.println(name.length());
}
Перед тем как выполнить метод, нам необходимо проверить значение на null
. Это долго и может привести к ошибкам в будущем. Где гарантия, что далее в коде мы не будем использовать эту переменную и не забудем сделать проверку на null
потом?
Тогда, во время выполнения, программа рано или поздно упадет с NullPointerException
. Класс Optional
позволяет нам лучше и качественнее работать с null-значениями. Посмотрим как можно улучшить этот кусочек кода и перепишем его в функциональном стиле:
@Test
public void givenOptional_whenIfPresentWorks_thenCorrect() {
Optional<String> opt = Optional.ofNullable("Wine again");
opt.ifPresent(name -> System.out.println(name.length()));
}
1 строчка - оборачиваем объект в
Optional
2 строчка - валидация значения и исполнение кода
Значение по умолчанию с orElse()
Метод orElse()
используется для получения значения, которое находится внутри Optional
. Он принимает один параметр - значение по умолчанию. Таким образом orElse()
возвращает или само значение из Optional
или переданный аргумент:
@Test
public void whenOrElseWorks_thenCorrect() {
String nullName = null;
String name = Optional.ofNullable(nullName).orElse("Boris");
assertEquals("Boris", name);
}
Значение по умолчанию с orElseGet()
Метод orElseGet()
очень похож на метод orElse()
. Однако, вместо значения по умолчанию, которое передается в качестве аргумента, метод принимает функциональный интерфейс Supplier
, который вызывает определенное действие. Посмотрим на пример:
@Test
public void whenOrElseGetWorks_thenCorrect() {
String nullName = null;
String name = Optional.ofNullable(nullName).orElseGet(new Supplier<String>() {
@Override
public String get() {
return "Boris";
}
});
assertEquals("Boris", name);
}
В примере для наглядности мы создаем анонимный класс. Поскольку мы находимся в парадигме функционального программирования, код можно упростить, используя лямбду:
String name = Optional.ofNullable(nullName).orElseGet(() -> "Boris");
В чем отличие orElseGet() от orElse()
На первый взгляд, разницы нет. По крайней мере, создается такое впечатление, не так ли? Тем не менее, методы отличаются. Давайте попробуем это понять на примерах. Создадим метод, который ничего не принимает и возвращает значение по умолчанию:
public String getMyDefault() {
System.out.println("А не выпить ли бокальчик?");
return "Вера, надежда, вино";
}
Напишем теперь два теста с использованием методов orElseGet()
и orElse()
:
@Test
public void whenOrElseGetAndOrElseOverlap_thenCorrect() {
String text = null;
String defaultText = Optional.ofNullable(text).orElseGet(this::getMyDefault);
assertEquals("Вера, надежда, вино", defaultText);
defaultText = Optional.ofNullable(text).orElse(getMyDefault());
assertEquals("Вера, надежда, вино", defaultText);
}
В этом примере мы оборачиваем null-объект в Optional
и пытаемся получить его значение с orElseGet()
и orElse()
. В обоих случаях "побочный" эффект одинаков:
А не выпить ли бокальчик?
А не выпить ли бокальчик?
Метод getMyDefault()
вызывается и для orElseGet()
, и для orElse()
. Когда значение внутри Optional
отсутствует, методы работают идентично.
Теперь напишем еще один тест, в котором оборачиваемый объект будет не null
и в котором значение по умолчанию, в идеале, не должно создаваться.
@Test
public void whenOrElseGetAndOrElseDiffer_thenCorrect() {
String text = "Вино не может быть null!";
System.out.print("Метод orElseGet:");
String defaultText
= Optional.ofNullable(text).orElseGet(this::getMyDefault);
assertEquals("Вино не может быть null!", defaultText);
System.out.print("\nМетод orElse:");
defaultText = Optional.ofNullable(text).orElse(getMyDefault());
assertEquals("Вино не может быть null!", defaultText);
}
А теперь заглянем в консоль:
Метод orElseGet:
Метод orElse:А не выпить ли бокальчик?
Обратите внимание, что в случае метода orElseGet()
метод getMyDefault()
даже не вызывается. Но в случае использования метода orElse()
объект по умолчанию создается и он ни разу даже не используется.
В этом простом примере создание объекта по умолчанию не является для нас чем-то "дорогим", но что если метод getMyDefault()
должен выполнить какой-то тяжелый и "дорогостоящий" запрос к БД или другому сервису?
Исключения с orElseThrow()
Метод orElseThrow()
вместо предоставления дефолтного метода выбрасывает исключение:
@Test(expected = IllegalArgumentException.class)
public void whenOrElseThrowWorks_thenCorrect() {
String nullName = null;
String name = Optional.ofNullable(nullName).orElseThrow(
IllegalArgumentException::new);
}
С Java 10 orElseThrow()
поддерживает пустой конструктор. В случае, если Optional
содержит null
, выбрасывается NoSuchElementException
.
@Test(expected = NoSuchElementException.class)
public void whenNoArgOrElseThrowWorks_thenCorrect() {
String nullName = null;
String name = Optional.ofNullable(nullName).orElseThrow();
}
Получение значения с помощью get()
Еще один способ получить значение из Optional
- вызвать метод get()
:
@Test
public void givenOptional_whenGetsValue_thenCorrect() {
Optional<String> opt = Optional.of("Wine");
String name = opt.get();
assertEquals("Wine", name);
}
В отличие от предыдущих способов get()
возвращает значение только если оно не null
. В противном случае выбрасывается NoSuchElementException
.
@Test(expected = NoSuchElementException.class)
public void givenOptionalWithNull_whenGetThrowsException_thenCorrect() {
Optional<String> opt = Optional.ofNullable(null);
String name = opt.get();
}
Условия: метод filter()
Метод filter()
принимает предикат в качестве аргумента и возвращает объект Optional
. Если значение проходит условие, заданное предикатом, то Optional
возвращается без изменений. Если предикат возвращает false
, то на выходе мы получим пустой Optional
:
@Test
public void whenOptionalFilterWorks_thenCorrect() {
Integer year = 2022;
Optional<Integer> yearOptional = Optional.of(year);
boolean is2016 = yearOptional.filter(y -> y == 2022).isPresent();
assertTrue(is2016);
boolean is2017 = yearOptional.filter(y -> y == 2021).isPresent();
assertFalse(is2017);
}
Метод filter()
используется для отбраковки обернутых значений по заранее определенным правилам.
Давайте еще посмотрим на один пример. Мы хотим купить наушники и нас интересует только их цена. Мы получаем push-уведомления о ценах на наушники с определенного сайта и храним их в объектах:
class Headphones {
private Double price;
public Headphones(Double price) {
this.price = price;
}
// геттеры и сеттеры
}
Теперь мы передаем этот объект какому-то коду, единственная цель которого проверить, подходит ли он нам по цене. Как это будет без Optional
:
public boolean priceIsInRange1(Headphones headphones) {
boolean isInRange = false;
if (headphones != null && headphones.getPrice() != null
&& (headphones.getPrice() >= 10
&& headphones.getPrice() <= 15)) {
isInRange = true;
}
return isInRange;
}
Посмотрите сколько кода... Значимых строчек только две - 5 и 6, остальные проверочные
@Test
public void whenFiltersWithoutOptional_thenCorrect() {
assertTrue(priceIsInRange1(new Headphones(10.0)));
assertFalse(priceIsInRange1(new Headphones(9.9)));
assertFalse(priceIsInRange1(new Headphones(null)));
assertFalse(priceIsInRange1(new Headphones(15.5)));
assertFalse(priceIsInRange1(null));
}
Теперь попробуем про все это забыть и сделать с помощью Optional
и его метода filter()
:
public boolean priceIsInRange2(Headphones headphones) {
return Optional.ofNullable(headphones)
.map(Headphones::getPrice)
.filter(p -> p >= 10)
.filter(p -> p <= 15)
.isPresent();
}
Метод map()
используется для преобразования одного значения в другое. Он не меняет оригинального значения. В итоге мы получаем optional-объект цены.
Во-первых, нас теперь больше не волнует возможное появление null-значений.
Во-вторых, нам пришлось писать только указанную в названии метода логику - проверять цену. Все остальное за нас делает Optional
.
@Test
public void whenFiltersWithOptional_thenCorrect() {
assertTrue(priceIsInRange2(new Headphones(10.0)));
assertFalse(priceIsInRange2(new Headphones(9.9)));
assertFalse(priceIsInRange2(new Headphones(null)));
assertFalse(priceIsInRange2(new Headphones(15.5)));
assertFalse(priceIsInRange2(null));
}
Согласитесь, решение с Optional
в данном примере намного более компактное и избавляет нас от лишних проверок, не связанных с основной логикой.
Изменение значения: метод map()
В прошлом разделе мы увидели как проверить значение на соответствие некоторым условиям. С помощью метода map()
можно изменить и само значение, и его тип.
@Test
public void givenOptional_whenMapWorks_thenCorrect() {
List<String> companyNames = Arrays.asList(
"paypal", "oracle", "", "microsoft", "", "apple");
Optional<List<String>> listOptional = Optional.of(companyNames);
int size = listOptional
.map(List::size)
.orElse(0);
assertEquals(6, size);
}
Действие, которое мы выполняем, заключается в получении значения размера списка элементов из самого списка. Метод map()
возвращает результат вычислений, завернутый внутрь Optional
. Затем нам нужно вызвать соответствующий метод, чтобы получить его значение.
Обратите внимание на разницу. Метод filter()
просто проверяет значение и возвращает Optional
с объектом того же типа, если оно соответствует заданному предикату. В противном случае возвращается пустой Optional
. Метод map()
берет существующее значение, выполняет вычисления и возвращает их результат, обернутый в объект Optional
:
@Test
public void givenOptional_whenMapWorks_thenCorrect2() {
String name = "Java";
Optional<String> nameOptional = Optional.of(name);
int length = nameOptional
.map(String::length)
.orElse(0);
assertEquals(4, length);
}
Можно комбинировать методы map()
и filter()
в цепочки, чтобы сделать какую-то более сложную операцию.
В следующем примере мы сначала очищаем пароль от лишних символов с помощью метода map()
и только потом делаем проверку filter()
:
@Test
public void givenOptional_whenMapWorksWithFilter_thenCorrect() {
String password = " password ";
Optional<String> passOpt = 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);
}
Без предварительной "очистки" наш пароль пройдет сквозь метод фильтра (первый assert).
Изменение значения: метод flatMap()
Разница между map()
и flatMap()
в том, что map()
изменяет только "распакованные" значения, а flatMap() перед изменением "распаковывает" значение самостоятельно. Чтобы это лучше понять, давайте сразу посмотрим на пример. Возьмем произвольный объект Person
c полями имя, возраст и пароль:
public class Person {
private String name;
private int age;
private String password;
public Optional<String> getName() {
return Optional.ofNullable(name);
}
public Optional<Integer> getAge() {
return Optional.ofNullable(age);
}
public Optional<String> getPassword() {
return Optional.ofNullable(password);
}
// конструкторы и сеттеры
}
Представим, что нам вернулся объект Person обернутый в Optional (или же мы сами его таким создали):
Person person = new Person("Владимир", 69);
Optional<Person> personOptional = Optional.of(person);
Теперь если мы попробуем получить поля этого объекта, они тоже будут будут обернуты в Optional:
@Test
public void givenOptional_whenFlatMapWorks_thenCorrect2() {
Person person = new Person("Владимир", 69);
Optional<Person> personOptional = Optional.of(person);
Optional<Optional<String>> nameOptionalWrapper
= personOptional.map(Person::getName);
Optional<String> nameOptional
= nameOptionalWrapper.orElseThrow(IllegalArgumentException::new);
String name1 = nameOptional.orElse("");
assertEquals("Владимир", name1);
String name = personOptional
.flatMap(Person::getName)
.orElse("");
assertEquals("Владимир", name);
}
Обратите внимание, что в этом примере мы получаем доступ к полю объекта двумя путями: с помощью map()
и flatMap()
.
Использование map()
оборачивает в Optional
то, что мы получаем вызовом getName
. В итоге нам приходится делать дополнительную "распаковку". FlatMap()
же делает это неявно за нас.
Цепочки Optional в Java 8
Иногда нам может понадобиться получить первый непустой объект Optional из нескольких. Давайте сразу обратимся к примеру и посмотрим на методы, которые будем использовать далее:
private Optional<String> getEmpty() {
return Optional.empty();
}
private Optional<String> getHello() {
return Optional.of("hello");
}
private Optional<String> getBye() {
return Optional.of("bye");
}
private Optional<String> createOptional(String input) {
if (input == null || "".equals(input) || "empty".equals(input)) {
return Optional.empty();
}
return Optional.of(input);
}
Используем Stream API, чтобы сформировать цепочку Optional
и вернуть первый непустой элемент:
@Test
public void givenThreeOptionals_whenChaining_thenFirstNonEmptyIsReturned() {
Optional<String> found = Stream.of(getEmpty(), getHello(), getBye())
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();
assertEquals(getHello(), found);
}
Недостатком такого метода является то, что все наши get-методы всегда исполняются, независимо от того где мы встретим затем в потоке непустой объект.
Если мы хотим воспользоваться "ленивой" обработкой, нам нужно использовать ссылку на метод и интерфейс Supplier
:
@Test
public void givenThreeOptionals_whenChaining_thenFirstNonEmptyIsReturnedAndRestNotEvaluated() {
Optional<String> found =
Stream.<Supplier<Optional<String>>>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() {
Optional<String> found = Stream.<Supplier<Optional<String>>>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.<Supplier<Optional<String>>>of(
() -> createOptional("empty"),
() -> createOptional("empty")
)
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.orElseGet(() -> "default");
assertEquals("default", found);
}
JDK 9 и Optional API
Начиная с 9-ки в Optional
добавилось несколько новых методов:
метод
or()
с помощьюSupplier
создает альтернативныйOptional
;метод
ifPresentOrElse()
позволяет выполнить действие, еслиOptional
не пустой, и другое действие, если пустой;метод
stream()
преобразуетOptional
в поток.
Некорректное использование Optional
Представьте, что у нас есть лист Person
и мы хотим использовать метод, который будет искать людей по имени. Кроме этого мы хотим, чтобы можно было искать и по возрасту, если он указан. Получится такой код:
public static List<Person> search(List<Person> people, String name, Optional<Integer> age) {
// проверки на null для people и 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<Person> search(List<Person> people, String name, Integer age) {
// проверки на null для people и 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<Person> search(List<Person> people, String name) {
return doSearch(people, name, 0);
}
public static List<Person> search(List<Person> people, String name, int age) {
return doSearch(people, name, age);
}
private static List<Person> doSearch(List<Person> people, String name, int age) {
// проверки на null для people и name
return people.stream()
.filter(p -> p.getName().equals(name))
.filter(p -> p.getAge().get().intValue() >= age)
.collect(Collectors.toList());
}
Существуют решения, позволяющие избежать использования Optional
в качестве параметров метода. Запомните, что Optional
появилось в Java, чтобы его использовали в качестве возвращаемого типа, таким образом указывая, что метод может вернуть пустое значение.
Важно помнить
Несмотря на большое количество возможностей, предоставляемых классом Optional
, злоупотреблять ими тоже не стоит.
Старайтесь избегать ситуаций с возвратом
null
из методов. Большинство клиентов вашего API ожидают получать безопасный ответ. Возвратnull
может привести кNullPointerException.
Вместо этого можно вернутьOptional.empty.
Старайтесь не использовать
Optional
в качестве параметра в методах. Если такое происходит, то придется делать лишнюю обертку для аргумента. Это ухудшит читаемость кода. Если параметра в методе может не быть, то лучше использовать перегруженную версию метода.В большинстве ситуаций удобнее использовать
Optional.map
иOptional.flatMap
вместоOptional.ifPresent
, чтобы избежать лишних проверок.Optional
не имплементирует интерфейсSerializable
, поэтому лучше не использовать его в качестве полей класса. Основная его цель - использоваться в качестве возвращаемого значения, если такое значение может отсутствовать.Не злоупотребляйте использованием
Optional
.
Послесловие
Я начинающий Java-разработчик. В рамках развития своей карьеры я решил сделать две вещи:
Завести канал в ТГ, где буду рассказывать о себе, своем пути и проблемах, с которыми сталкиваюсь - https://t.me/java_wine
Завести блог на Хабре, куда буду выкладывать материалы, которые использую для своего обучения и развития, которые лично мне помогли разобраться с той или иной темой.
Надеюсь, буду полезен сообществу и новичкам, которые пойдут по моим или чьим-то еще стопам.
Спасибо!