Вступление
При написании любого кода на Java разработчики чаще работают с
объектами, чем с примитивными значениями ( int
, boolean
и т. Д.).
Это связано с тем, что объекты составляют самую суть
объектно-ориентированного программирования: они позволяют программисту
писать абстрактный код в чистой и структурированной манере.
Более того, каждый объект в Java может либо содержать значение, либо
нет. Если это так, его значение сохраняется в куче, а переменная,
которую мы используем, имеет ссылку на этот объект. Если объект не
содержит значения, по умолчанию используется значение null
-
специальный заполнитель, обозначающий отсутствие значения.
Тот факт, что каждый объект может стать null
, в сочетании с
естественной тенденцией использовать объекты вместо примитивов,
означает, что некоторый произвольный фрагмент кода может (а часто и
будет) привести к неожиданному NullPointerException
.
До того, как в Java 8 Optional
NullPointerException
были гораздо
более распространены в повседневной жизни Java-программистов.
В следующих разделах мы углубимся в объяснение Optional
и увидим, как
его можно использовать для преодоления некоторых распространенных
проблем, связанных с нулевыми значениями.
Необязательный класс
Необязательный
элемент - это, по сути, контейнер. Он предназначен либо для хранения
значения, либо для того, чтобы быть «пустым», если значение не
существует - замена null
значения. Как мы увидим в некоторых более
поздних примерах, эта замена имеет решающее значение, поскольку она
позволяет неявную проверку нуля для каждого объекта, представленного как
Optional
.
Это означает, что с точки зрения программиста явная проверка на нуль больше не требуется - она выполняется самим языком.
Создание опционалов
Давайте посмотрим, насколько просто создать экземпляры Optional
и
обернуть объекты, которые у нас уже есть в наших приложениях.
Мы будем использовать для этого наш собственный класс, класс Spaceship
public class Spaceship {
private Engine engine;
private String pilot;
// Constructor, Getters and Setters
}
А наш Engine
выглядит так:
public class Engine {
private VelocityMonitor monitor;
// Constructor, Getters and Setters
}
И, кроме того, у нас есть класс VelocityMonitor
public class VelocityMonitor {
private int speed;
// Constructor, Getters and Setters
}
Эти классы произвольны и служат только для пояснения, за ними нет реальной реализации.
из()
Первый подход к созданию Optional
s - это использование .of()
,
передача ссылки на ненулевой объект:
Spaceship falcon = new Spaceship();
Optional<Spaceship> optionalFalcon = Optional.of(falcon);
Если falcon
был null
, метод .of()
выбросил бы
NullPointerException
.
Без Optional
, попытка доступа к любому из полей или методов falcon
(при условии, что оно null
) без выполнения нулевой проверки приведет
к сбою программы.
При использовании Optional
метод .of()
замечает null
значение и
NullPointerException
также может привести к сбою программы.
Если программа дает сбой при обоих подходах, зачем вообще использовать
Optional
?
Программа не выйдет из строя где-нибудь в глубине кода (при доступе к
falcon
), но при самом первом использовании (инициализации) null
объекта, минимизируя потенциальный ущерб.
ofNullable ()
Если falcon
разрешено быть null
, вместо .of()
метод, мы будем
использовать .ofNullable()
метод. Они работают так же, если значение
не равно null
. Разница очевидна, когда ссылка указывает на null
в
этом случае метод .ofNullable()
совершенно не уважает этот фрагмент
кода:
Spaceship falcon = null;
Optional<Spaceship> optionalFalcon = Optional.ofNullable(falcon);
пустой()
И, наконец, вместо обертывания существующей ссылочной переменной (
null
или null
) мы можем создать null
значение в контексте
Optional
. Это похоже на пустой контейнер, который возвращает пустой
экземпляр Optional
:
Optional<Spaceship> emptyFalcon = Optional.empty();
Проверка значений
После создания Optional
и упаковки в них информации, вполне
естественно, что мы захотим получить к ним доступ.
Однако, прежде чем получить доступ, мы должны проверить, есть ли
какие-либо значения, или если Optional
s пустые.
настоящее()
Поскольку перехват исключений - сложная операция, было бы лучше использовать один из методов API, чтобы проверить, существует ли значение, прежде чем пытаться получить к нему доступ - и изменить поток, если это не так.
Если это так, то для доступа к значению можно использовать метод
.get()
Впрочем, подробнее об этом методе в последних разделах.
Чтобы проверить, присутствует ли значение внутри Optional
, мы
используем метод .isPresent()
. По сути, это замена null
проверки:
// Without Optional
Spaceship falcon = hangar.getFalcon();
if (falcon != null) {
System.out.println(falcon.get());
} else {
System.out.printn("The Millennium Falcon is out and about!");
}
// With Optional
Optional<Spaceship> optionalFalcon = Optional.ofNullable(hangar.getFalcon());
if (optionalFalcon.isPresent()) {
System.out.println(falcon.get());
} else {
System.out.println("The Millennium Falcon is out and about!");
}
Поскольку falcon
тоже не может находиться в ангаре, мы также можем
ожидать null
значение, поэтому используется .ofNullable()
.
если представить()
Чтобы упростить задачу, Optional
также содержит условный метод,
который полностью обходит проверку присутствия:
Optional<Spaceship> optionalFalcon = Optional.ofNullable(hangar.getFalcon());
optionalFalcon.ifPresent(System.out::println);
Если значение присутствует, содержимое печатается через
ссылку на метод. Если в контейнере нет
значения, ничего не происходит. Тем не менее, вы все равно можете
использовать предыдущий подход, если хотите определить оператор
else {}
.
Это отражает то , что мы уже упоминали ранее , когда мы сказали , что
null
-Проверяет с Optional
подразумеваются и применяются по типу
конструкции системы.
пустой()
Другой способ проверить значение - использовать .isEmpty()
. По сути,
вызов Optional.isEmpty()
аналогичен вызову !Optional.isPresent()
.
Особой разницы нет:
Optional<Spaceship> optionalFalcon = Optional.ofNullable(hangar.getFalcon());
if (optionalFalcon.isEmpty()) {
System.out.println("Please check if the Millennium Falcon has returned in 5 minutes.");
} else {
optionalFalcon.doSomething();
}
Вложенные нулевые проверки
Наш Spaceship
, как определено ранее, имеет атрибут Engine
, у
которого есть атрибут VelocityMonitor
.
Предположим теперь, что мы хотим получить доступ к объекту монитора
скорости и получить текущую скорость космического корабля, принимая во
внимание, что все эти значения потенциально могут быть null
.
Получение скорости может выглядеть примерно так:
if (falcon != null) {
Engine engine = falcon.getEngine();
if (engine != null) {
VelocityMonitor monitor = engine.getVelocityMonitor();
if (monitor != null) {
Velocity velocity = monitor.getVelocity();
System.out.println(velocity);
}
}
}
Приведенный выше пример показывает, насколько утомительно выполнять такие проверки, не говоря уже о количестве шаблонного кода, необходимого для того, чтобы в первую очередь сделать эти проверки возможными.
Альтернативным решением с использованием Optional
было бы:
Velocity velocity = falcon
.flatMap(Spaceship::getEngine)
.flatMap(Engine::getVelocityMonitor)
.map(VelocityMonitor::getVelocity);
Примечание . Не уверены, что происходит выше? Ознакомьтесь с объяснением ниже, чтобы узнать подробности.
При использовании такого подхода никаких явных проверок не требуется.
Если какой-либо из объектов содержит пустой Optional
, конечный
результат также будет пустым Optional
.
Чтобы все работало так, нам нужно изменить существующие определения
классов Spaceship
и Engine
public class Spaceship {
private Optional<Engine> engine;
private String pilot;
// Constructor, Getters and Setters
}
public class Engine {
private Optional<VelocityMonitor> monitor;
// Constructor, Getters and Setters
}
Что мы изменили, так это определения атрибутов: теперь они заключены в
Optional
объекты, чтобы сделать возможным такое альтернативное
решение.
Поначалу это может показаться немного утомительным, но если это спланировано с самого начала, то потребуется почти столько же усилий, чтобы набрать его.
Кроме того, наличие Optional
атрибута вместо обычного объекта отражает
тот факт, что атрибут может существовать, а может и не существовать.
Обратите внимание, как это очень полезно, поскольку у нас нет
семантических значений такого рода с обычными определениями атрибутов.
Пример объяснения
flatMaps
немного времени, чтобы объяснить предыдущий пример с maps
и
картами. Если вы все поняли без дополнительных объяснений, пропустите
этот раздел.
Первый вызов метода выполняется для falcon
который имеет тип
Optional<Spaceship>
. Вызов getEngine
возвращает объект типа
Optional<Engine>
. При объединении этих двух типов тип возвращаемого
объекта становится Optional<Optional<Engine>>
.
Поскольку мы хотели бы рассматривать этот объект как Engine
и
выполнять с ним дальнейшие вызовы, нам нужен какой-то механизм для
«снятия» внешнего Optional
слоя.
Такой механизм существует, и он называется flatMap
. Этот метод API
объединяет map
и flat
операции, сначала применяя функцию к каждому
из элементов, а затем сглаживая результат в одноуровневый поток.
С map
только применяет функцию без выравнивания потока. В нашем случае
использование map
и flatMap
даст нам Optional<Optional<Engine>>
и
Optional<Engine>
соответственно.
Вызов flatMap
для объекта типа Optional
, следовательно, приведет к
одноуровневому Optional
, что позволит нам использовать несколько
похожих вызовов методов подряд.
В итоге у нас Optional<Engine>
, который нам нужен в первую очередь.
Альтернативные результаты
.orElse ()
Предыдущий пример можно расширить с помощью orElse(T other)
. Метод
вернет Optional
объект, для которого он вызывается, только если в нем
содержится значение.
Если Optional
пуст, метод возвращает other
значение. По сути, это
Optional
версия тернарного оператора:
// Ternary Operator
Spaceship falcon = maybeFalcon != null ? maybeFalcon : new Spaceship("Millennium Falcon");
// Optional and orElse()
Spaceship falcon = maybeFalcon.orElse(new Spaceship("Millennium Falcon"));
Как и в случае с ifPresent()
, в этом подходе используются
лямбда-выражения, чтобы сделать код
более читаемым и менее подверженным ошибкам.
.orElseGet ()
Вместо того, чтобы other
значение в качестве аргумента, мы можем
использовать
Поставщик
. Разница между .orElse()
и .orElseGet()
, хотя и не очевидна на
первый взгляд, существует:
// orElse()
Spaceship falcon = maybeFalcon.orElse(new Spaceship("Millennium Falcon"));
// orElseGet()
Spaceship falcon = maybeFalcon.orElseGet(() -> new Spaceship("Millennium Falcon"));
Если maybeFalcon
не содержит значения, оба метода вернут новый
Spaceship
. В этом случае их поведение такое же. Разница становится
очевидной, если maybeFalcon
действительно содержит значение.
В первом случае new Spaceship
не будет возвращен, но он будет создан
. Это произойдет независимо от того, существует ли значение. Во втором
случае new Spaceship
будет создан только в том случае, если
возможно, что maybeFalcon
не содержит значения.
Это похоже на то, как do-while
в while
do-while
делает задачу
независимо от в while
цикла, по крайней мере , один раз.
Это может показаться незначительной разницей, но она становится очень важной, если создание космических кораблей - сложная операция. В первом случае мы всегда создаем новый объект - даже если он никогда не будет использоваться.
В таких случаях рекомендуется использовать .orElseGet()
.orElse()
.orElseThrow ()
Вместо того, чтобы возвращать альтернативное значение (как мы видели в
предыдущих двух разделах), мы можем вызвать исключение. Это достигается
с помощью .orElseThrow()
который вместо альтернативного значения
принимает поставщика, который возвращает исключение в случае его
возникновения.
Это может быть полезно в тех случаях, когда конечный результат очень важен и не должен быть пустым. Создание исключения в этом случае может быть самым безопасным вариантом:
// Throwing an exception
Spaceship falcon = maybeFalcon.orElseThrow(NoFuelException::new);
Получение значений из необязательного
.получать()
Увидев множество различных способов проверки и доступа к значению внутри
Optional
, давайте теперь рассмотрим один последний способ получения
значения, который также использует некоторые из ранее показанных
методов.
Самый простой способ получить доступ к значению внутри Optional
-
использовать .get()
. Этот метод возвращает текущее значение или
выдает NoSuchElementException
если значение отсутствует:
Optional<Spaceship> optionalFalcon = Optional.ofNullable(hangar.getFalcon());
if (falcon.isPresent()) {
Spaceship falcon = optionalFalcon.get()
// Fly the falcon
}
Как и ожидалось, метод .get()
возвращает null
экземпляр Spaceship
и назначает его объекту falcon
Заключение
Optional
был введен в Java как способ исправить проблемы с null
ссылками. До Optional
каждому объекту разрешалось либо содержать
значение, либо нет (то есть иметь значение null
).
Введение Optional
сути, требует null
системой типов, что делает
ненужным выполнение таких проверок вручную.
Это был большой шаг как в улучшении языка, так и в его удобстве
использования за счет добавления дополнительного уровня проверки типов.
Использование этой системы вместо старомодной null
позволяет писать
четкий и лаконичный код без необходимости добавлять шаблон и выполнять
утомительные проверки вручную.