Руководство по необязательному в Java 8

Введение При написании любого кода на Java разработчики чаще работают с объектами, чем с примитивными значениями (int, boolean и т. Д.). Это связано с тем, что объекты составляют самую суть объектно-ориентированного программирования: они позволяют программисту писать абстрактный код в чистой и структурированной манере. Более того, каждый объект в Java может либо содержать значение, либо нет. Если это так, его значение сохраняется в куче, а переменная, которую мы используем, имеет ссылку на этот объект. Если

Вступление

При написании любого кода на 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 позволяет писать четкий и лаконичный код без необходимости добавлять шаблон и выполнять утомительные проверки вручную.

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus

Содержание