В 1964 году британский ученый-компьютерщик Тони Хоар изобрел ссылки на нулевые указатели (Null Pointer References).
Исключение Null Pointer Exception составляет львиную долю всех исключений, которые возникают в продакшне. Оно было реализовано во многих языках программирования, включая C, C++, C#, JavaScript, Java и другие.
Потеря денег, времени и человеческих ресурсов, которые уходят на его исправление, побудила Хоара назвать его “ошибкой на миллиард долларов”.
Java — один из языков программирования, где реализованы ссылки на нулевые указатели. Если вы разрабатывали на Java, то наверняка их встречали. Неважно, новичок ли вы в Java или за плечами у вас десять лет опыта — всегда есть вероятность, что вы столкнетесь с ошибкой Null Pointer Exception.
Optional в Java
Optional
— это API, предоставленный в Java 8. При правильном применении он способен решить проблему Null Pointer Exception.
API Optional
реализует функциональное программирование и использует функциональный интерфейс.
Прежде чем мы продолжим, пожалуйста, обратите внимание, что примеры в этой статье приводятся на Java 11. Если вы пользуетесь другой версией Java, некоторые методы могут не существовать или вести себя иначе.
Пустой Optional
Пустой элемент optional
— это основной способ избежать исключения Null Pointer Exception при использовании API Optional
.
В потоке Optional
любой null
будет преобразован в пустой Optional
. Пустой элемент Optional
больше не будет обрабатываться. Вот как мы можем избежать исключения NullPointerException
.
Подробнее о том, как ведет себя пустой Optional
, мы поговорим позже.
Создадим объект Optional
Существует три способа инициализации объекта Optional
:
Optional.of(T)
Optional.ofNullable(T)
Optional.empty()
Optional.of
Optional.of
принимает в качестве параметра любой тип с ненулевым значением. Чтобы создать объект Optional
с помощью Optional.of
, нужно просто передать значение в параметр.
@Test
public void initializeOptional_optionalOf() {
Optional<String> helloWorldOptional = Optional.of("Hello, world");
assert helloWorldOptional.isPresent();
assert "Hello, world".equals(helloWorldOptional.get());
}
Будьте осторожны, когда передаете значение Optional.of
. Помните, что Optional.of
не принимает в качестве параметра нулевые значения. Если вы попытаетесь передать нулевое значение, оно вызовет исключение NullPointerException
.
@Test
public void initializeOptional_optionalOf_null() {
try {
Optional.of(null);
} catch (Exception e) {
assert e instanceof NullPointerException;
}
}
Optional.ofNullable
Optional.ofNullable
аналогичен Optional.of
. Он принимает любой тип. Разница в том, что с помощью Optional.ofNullable
параметру возможно передать нулевое значение.
@Test
public void initializeOptional_optionalOfNullable() {
Optional<String> helloWorldOptional = Optional.ofNullable("Hello, world");
assert helloWorldOptional.isPresent();
assert "Hello, world".equals(helloWorldOptional.get());
}
Когда Optional.ofNullable
инициализируется с объектом null
, он возвращает пустой Optional
.
@Test
public void initializeOptional_optionalOfNullable_null() {
Optional<String> helloWorldOptional = Optional.ofNullable(null);
assert !helloWorldOptional.isPresent();
try {
helloWorldOptional.get();
} catch (Exception e) {
assert e instanceof NoSuchElementException;
}
}
Optional.empty
Пустой Optional
можно инициализировать через Optional.empty()
.
@Test
public void initializeOptional_optionalEmpty() {
Optional<String> helloWorldOptional = Optional.empty();
assert !helloWorldOptional.isPresent();
}
Доступ к Optional
.
Есть несколько способов получить значение Optional
.
get
Простой метод. Метод get
вернет значение Optional
, если оно присутствует, и вызовет исключение NoSuchElementException
, если значения не существует.
@Test
public void get_test() {
Optional<String> helloWorldOptional = Optional.of("Hello, World");
assert "Hello, World".equals(helloWorldOptional.get());
}
@Test
public void get_null_test() {
Optional<String> helloWorldOptional = Optional.empty();
try {
helloWorldOptional.get();
} catch (Exception e) {
assert e instanceof NoSuchElementException;
}
orElse
Если вы предпочитаете значение по умолчанию в случае, когда Optional
пуст, вы можете воспользоваться методом orElse
.
@Test
public void orElse_test() {
Optional<String> helloWorldOptional = Optional.of("Hello, World");
assert "Hello, World".equals(helloWorldOptional.orElse("default"));
}
@Test
public void orELseNull_test() {
Optional<String> helloWorldOptional = Optional.empty();
assert "default".equals(helloWorldOptional.orElse("default"));
orElseGet
orElseGet
очень похож на метод orElse
. Только orElseGet
принимает в качестве параметра Supplier<T>
.
@Test
public void orElseGet_test() {
Optional<String> helloWorldOptional = Optional.of("Hello, World");
assert "Hello, World".equals(helloWorldOptional.orElseGet(() ->"default"));
}
@Test
public void orELseGet_Null_test() {
Optional<String> helloWorldOptional = Optional.empty();
assert "default".equals(helloWorldOptional.orElseGet(() ->"default"));
}
orElseThrow
orElseThrow
вернет значение Optional
или выдаст исключение, если значение Optional
пустое.
@Test
public void orElseThrow_test() {
Optional<String> helloWorldOptional = Optional.of("Hello, World");
assert "Hello, World".equals(helloWorldOptional.orElseThrow(NullPointerException::new));
}
@Test
public void orELseThrow_Null_test() {
Optional<String> helloWorldOptional = Optional.empty();
try {
helloWorldOptional.orElseThrow(NullPointerException::new);
} catch (Exception e) {
assert e instanceof NullPointerException;
}
}
Обработка Optional
Есть много способов обработки и преобразования Optional
. В этом разделе мы рассмотрим наиболее распространенные методы.
Как уже говорилось в начале статьи, пустой элемент Optional
не будет обрабатываться в потоке. Сейчас мы увидим это на примерах.
map
Метод map
чаще других применяется при обработке объекта Optional
. В качестве параметра он принимает Function<? super T, ? extends U>
и возвращает Optional<U>
. Это означает, что в вашей функции может быть любой тип параметра, а возвращаемое значение будет обернуто в Optional
внутри метода map
.
@Test
public void processingOptional_map_test() {
Optional<String> stringOptional = Optional.of("Hello, World")
.map(a -> a + ", Hello");
assert stringOptional.isPresent();
assert "Hello, World, Hello".equals(stringOptional.get());
}
Если вы попытаетесь вернуть нулевое значение в функции <? super T,? extends U>
, метод map
вернет пустой Optional
.
@Test
public void processingOptional_map_empty_test() {
Optional<String> stringOptional = Optional.of("Hello, World")
.map(a -> null);
assert !stringOptional.isPresent();
}
Пустой Optional
не будет обработан map
. Это подтверждается следующим тестом:
@Test
public void processingOptional_map_empty_notProcessed_test() {
AtomicBoolean atomicBoolean = new AtomicBoolean(false);
Optional<String> stringOptional = Optional.of("Hello, World")
.map(a -> null)
.map(a -> {
atomicBoolean.set(true);
return "won't be processed";
});
assert !stringOptional.isPresent();
assert atomicBoolean.get() == false;
}
flatMap
Этот метод похож на map
, но flatMap
не будет переносить возвращаемое значение функции в Optional
. Метод flatMap
принимает в качестве параметра Function<? super T, ? extends Optional<? extends U>>
. Это означает, что вам нужно будет определить функцию, которая принимает любой тип и возвращает Optional
.
Как правило, метод flatMap
пригождается, когда ваш код вызывает другой метод, возвращающий объект Optional
.
@Test
public void processingOptional_flatmap_test() {
Optional<String> stringOptional = Optional.of("Hello, World")
.flatMap(this::getString);
assert "Hello, World, Hello".equals(stringOptional.get());
}
@Test
public void processingOptional_flatmap_randomString_test() {
Optional<String> stringOptional = Optional.of(UUID.randomUUID().toString())
.flatMap(this::getString);
assert !stringOptional.isPresent();
}
public Optional<String> getString(String s) {
if ("Hello, World".equals(s)) {
return Optional.of("Hello, World, Hello");
}
return Optional.empty();
}
filter
В предыдущем примере с flatMap
мы использовали декларативный стиль для дифференциации возвращаемого значения метода getString
. Но можно переписать это в функциональном стиле с помощью метода filter
.
@Test
public void processingOptional_filter_test() {
Optional<String> stringOptional = Optional.of("Hello, World")
.filter(helloWorldString -> "Hello, World".equals(helloWorldString))
.map(helloWorldString -> helloWorldString + ", Hello");
assert "Hello, World, Hello".equals(stringOptional.get());
}
@Test
public void processingOptional_filter_randomString_test() {
Optional<String> stringOptional = Optional.of(UUID.randomUUID().toString())
.filter(helloWorldString -> "Hello, World".equals(helloWorldString))
.map(helloWorldString -> helloWorldString + ", Hello");
assert !stringOptional.isPresent();
}
ifPresent
Метод ifPresent
принимает Consumer
, который будет выполняться только в том случае, если Optional
не пуст.
@Test
public void processingOptional_ifPresent_test() {
AtomicBoolean atomicBoolean = new AtomicBoolean(false);
Optional.of("Hello, World")
.ifPresent(helloWorldString -> atomicBoolean.set(true));
assert atomicBoolean.get();
}
@Test
public void processingOptional_ifPresent_empty_test() {
AtomicBoolean atomicBoolean = new AtomicBoolean(false);
Optional.empty()
.ifPresent(helloWorldString -> atomicBoolean.set(true));
assert !atomicBoolean.get();
}
Чего следует избегать
Если вы хотите использовать Optional
в своем коде, вам стоит избегать некоторых критичных вещей.
Не создавайте метод, который принимает Optional
Создание метода, который принимает Optional
в качестве параметра, может привести к возникновению той самой проблемы, которую он предположительно решает: NullPointerException
.
Если пользователь метода с параметром Optional
не знает об этом, он может передать методу null
вместо Optional.empty()
. Обработка null
приведет к исключению NullPointerException
.
@Test
public void optionalAsParameter_test() {
try {
isPhoneNumberPresent(null);
} catch (Exception e) {
assert e instanceof NullPointerException;
}
}
public boolean isPhoneNumberPresent(Optional<String> phoneNumber) {
return phoneNumber.isPresent();
}
Получение значения без проверки
Если вы задействуете Optional
, то лучше по возможности избегать метода get
. Если по какой-то причине он вам все-таки нужен, убедитесь, что вы сначала проверили его с помощью метода isPresent
, потому что если применить get
на пустом Optional
, он вызовет исключение NoSuchMethodException
.
@Test
public void getWithIsPresent_test() {
Optional<String> helloWorldOptional = Optional.ofNullable(null);
if (helloWorldOptional.isPresent()) {
System.out.println(helloWorldOptional.get());
}
}
@Test
public void getWithoutIsPresent_error_test() {
Optional<String> helloWorldOptional = Optional.ofNullable(null);
try {
System.out.println(helloWorldOptional.get());
} catch (Exception e) {
assert e instanceof NoSuchElementException;
}
}
Заключение
Спасибо, что дочитали до конца! Optional
— мощная функция, о которой стоит знать каждому Java-разработчику. Если вы станете правильно применять функции optional
от начала до конца, то вряд ли еще когда-либо столкнетесь с исключением NullPointerException
.
Optional
также задействован в качестве базы других больших библиотек, таких как Reactor и RXJava, поэтому знание того, как работает Optional
, поможет вам разобраться в них тоже.
Репозиторий с примерами из этой статьи вы можете найти здесь: https://github.com/brilianfird/java-optional
Читайте также:
- Графовое моделирование данных на Java
- Java. Вложенные классы
- Портируем решатель судоку с Java на WebAssembly
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Brilian Firdaus: “Avoiding the Null Pointer Exception With Optional in Java”