Разработчики постоянно используют чужие библиотеки. Для доступа к базе данных часто применяют фреймворки Spring Data Jpa + Hibernate, которые организуют соединение с базой данных и базовый CRUD.
Допустим, мы хотим найти пользователя в базе данных по идентификатору. После чего нам требуется вывести на консоль длину имени пользователя. На первый взгляд простой и безобидный пример. Объект userRepository
, в данном случае, нам предоставляет фреймворк:
final User user = userRepository.findById(1L);
final String firstName = user.getFirstName();
System.out.println("Длинна твоего имени: " + firstName.length());
Запустим этот код мы можем получить исключение:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "dev.struchkov.example.tamtam.User.getFirstName()" because "user" is null
at dev.struchkov.example.tamtam.Test.test(Test.java:18)
at dev.struchkov.example.tamtam.Test.main(Test.java:13)
Почему это могло произойти? Дело в том, что мы не учли того факта, что пользователя с идентификатором 1L может не быть в базе данных. И фреймворку не остается ничего, кроме как вернуть нам null
, а во время вызова метода null
-объекта вы получите NullPointerException
.
NullPointerException
это головная боль новичков в программировании. Но неважно, новичок ли вы в Java или за плечами у вас десять лет опыта — всегда есть вероятность, что вы столкнетесь с NullPointerException
.
Чтобы придать некоторый исторический контекст, скажу, что Тони Хоар, написал: "Изобретение нулевой ссылки в 1965 году было мой ошибкой на миллиард долларов. Я не мог устоять перед искушением ввести нулевую ссылку просто потому, что ее было так легко реализовать".
А мы теперь имеем то, что имеем. Так что же делать в таком случае. Например, можно проверять все значения на null
, вот так:
final User user = userRepository.findById(1L);
if (user != null) {
final String firstName = user.getFirstName();
System.out.println("Длинна твоего имени: " + firstName.length());
}
Теперь если пользователя нет, то мы просто не выполняем код? Допустим, что пользователь с таким идентификатором все же есть, запускаем код
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "firstName" is null
at dev.struchkov.example.tamtam.Test.test(Test.java:20)
at dev.struchkov.example.tamtam.Test.main(Test.java:13)
И мы снова получили NullPointerException
, только на этот раз проблема оказалась в поле firstName
, теперь оно null
, и уже вызов метода length()
выбросил нулпойнтер. Не беда, добавим еще одну проверку:
final User user = userService.findById(1L);
if (user != null) {
final String firstName = user.getFirstName();
if (firstName != null) {
System.out.println("Длинна твоего имени: " + firstName.length());
}
}
Хм, где-то мы явно свернули не туда. Эти проверки просто мешают бизнес-логике и раздражают. Они снижают общую читаемость нашей программы.
Так приходилось жить до появления Optional
в Java. Что же имеем теперь?
Используемые версии
Java 17
Введение в Optional
Можно воспринимать Optional
, как некую коробку, обертку, в которую кладется какой-либо объект. Optional
всего лишь контейнер: он может содержать оюъект некоторого типа Т
, а может быть пустым, прямо как обычная коробка.

Перепишем наш пример с использованием Optional
и посмотрим, что изменится:
final Optional<User> optUser = userService.findById(1L);
if (optUser.isPresent()) {
final User user = optUser.get();
final String firstName = user.getFirstName();
if (firstName != null) {
System.out.println("Длинна твоего имени: " + firstName.length());
}
}
Вы скажете: это не выглядит проще, даже более того, добавилась лишняя строка?!! Но на самом деле случилось концептуально важное событие, вы точно знаете что метод findById()
возвращает контейнер, в котором объекта может не быть. Больше вы не ожидаете внезапного null значения, если только разработчик этого метода не сумашедший, ведь ничто не мешает ему вернуть null
вместо Optional
🤪
Здесь мы с вами увидели два основных метода: isPresent()
возвращает true, если внутри есть объект, а метод get()
возвращает этот объект. Да, проверку для firstName
пришлось оставить, но об этом позже.
Возвращаемся к нашему примеру, давайте упростим этот код с помощью других методов Optional
, чтобы увидеть какие преимущества нам это даст. Обо всех этих методах мы еще подробнее поговорим ниже.
final Optional<User> optUser = userService.findById(1L);
optUser.map(user -> user.getFirstName())
.ifPresent(
firstName -> System.out.println("Длинна твоего имени: " + firstName.length())
);
Мы воспользовались методом map()
, который преобразует наш Optional
в Optional
другого типа. В данном случае мы получили фамилию пользователя, то есть преобразовали Optional<User>
в Optional<String>
.
Вот такой вариант показывает это нагляднее:
final Optional<User> optUser = userService.findById(1L);
final Optional<String> optFirstName = optUser.map(user -> user.getFirstName());
optFirstName.ifPresent(
firstName -> System.out.println("Длинна твоего имени: " + firstName.length())
);
А дальше мы вызвали метод ifPresent()
, в котором мы вызвали вывод на консоль, можно еще чуть-чуть сократить код:
userService.findById(1L)
.map(User::getFirstName)
.ifPresent(
firstName -> System.out.println("Длинна твоего имени: " + firstName.length())
);
Методы Optional
Теперь, когда мы поняли зачем нам нужен Optional
и разобрали пример использования, давайте рассмотрим остальные методы этого класса. Optional
обладает большим количеством методов, правильное использование которых позволяет добиться более простого и понятного кода.
Cоздание Optional
До этого мы получали Optional
от кого-то, поэтому давайте узнаем, как создать Optional
своими руками. У данного класса нет конструкторов, но есть три статических метода, которые и создают экземпляры класса,
Метод Optional.ofNullable(T t)
Optional.ofNullable
принимает любой тип и создает контейнер. Если в параметры этого метода передать null
, то будет создан пустой контейнер.
@Test
@DisplayName("Создание Optional через ofNullable()")
void createOptionalByOfNullable() {
final String testString = "Test String";
final Optional<String> testOptional = Optional.ofNullable(testString);
...
}
Используйте этот метод, когда есть вероятность, что упаковываемый объект может иметь null
значение.
Метод Optional.of(T t)
Этот метод аналогичен Optional.ofNullable
, но если передать в параметр null
значение, то получим старый добрый NullPointerException
.
@Test
@DisplayName("Создание Optional через of()")
void createByOf() {
final String testString = "Test String";
final Optional<String> testOptional = Optional.of(testString);
assertEquals(testString, testOptional.get());
}
@Test
@DisplayName("Создание пустого Optional через of")
void createNullOptionalByOf() {
final String testString = null;
assertThrows(NullPointerException.class, () -> {
final Optional<String> testOptional = Optional.of(testString);
});
}
Используйте этот метод, когда точно уверены, что упаковываемое значение не должно быть null
.
Метод Optional.empty()
Иногда бывает нужно создать сразу пустой Optional
. Для этого существует метод Optional.empty()
.
Получение содержимого
Мы разобрали методы, которые позволяют нам создавать объект Optional
. Теперь изучим методы, которые позволят нам доставать значения из контейнера.
Метод isPresent()
Прежде, чем достать что-то, неплохо убедиться, что это что-то там действительно есть. И с этим нам поможет метод isPresent()
, который возвращает true
, если в контейнере есть объект и false
в противном случае.
Optional<User> optUser = userRepository.findById(1L);
if(optional.isPresent()) {
// если пользователь найден, то выполняем этот блок кода
}
Фактически это обычная проверка, как если бы мы сами написали if (value != null)
. И если зайти в реализацию метода, то это мы там и увидим:
public boolean isPresent() {
return value != null;
}
Метод isEmpty()
Метод isEmpty()
это противоположность методу isPresent()
. Метод вернет true
, если объекта внутри нет и false
в противном случае. Думаю, вы уже догадались, как выглядит реализация этого метода:
public boolean isEmpty() {
return value == null;
}
Метод get()
После того, как вы убедились в наличии объекта с помощью предыдущих методов, вы можете смело достать объект из контейнера с помощью метода get()
.
Optional<User> optUser = userRepository.findById(1L);
if(optional.isPresent()) {
final Person person = optPerson.get();
// остальной ваш код
}
Конечно, вы можете вызвать метод get()
и без проверки. Но если объекта там не окажется, то вы получите NoSuchElementException
.
@Test
@DisplayName("Вызываем get у пустого Optional")
void getForNullOptional() {
final String testString = null;
final Optional<String> testOptional = Optional.ofNullable(testString);
assertFalse(testOptional.isPresent());
assertThrows(NoSuchElementException.class, testOptional::get);
}
Метод ifPresent()
Помимо метода isPresent()
, имеется метод ifPresent()
, который принимает в качестве аргумента Consumer
лямбду. Это позволяет нам выполнить какую-то логику над объектом, если объект имеется.
@Test
@DisplayName("Проверка метода ifPresent()")
void useIfPresentMethod() {
final String testString = "test string";
final Optional<String> testOptional = Optional.of(testString);
final Consumer<String> ourConsumer = ourTestSpring -> {
System.out.println(ourTestSpring);
assertEquals(ourTestSpring, testString);
};
testOptional.ifPresent(ourConsumer);
}
Метод ifPresentOrElse()
Метод ifPresent()
ничего не сделает, если у вас нет объекта, но если вам и в этом случае необходимо выполнить какой-то код, то используйте метод ifPresentOrElse()
, который принимает в качестве параметра еще и Runnable
лямбду.
@Test
@DisplayName("Проверка метода ifPresentOrElse()")
void useIfPresentOrElse() {
final String testString = null;
final Optional<String> testOptional = Optional.ofNullable(testString);
testOptional.ifPresentOrElse(
ourTestSpring -> System.out.println(ourTestSpring),
() -> System.out.println("Null String in optional")
);
}
Метод orElse()
Метод делает ровно то, что ожидается от его названия: возвращает значение в контейнере или значение по-умолчанию, которое вы указали.
@Test
@DisplayName("Используем метод orElse")
void useGetOrElse() {
final String testString = null;
final Optional<String> testOptional = Optional.ofNullable(testString);
final String valueOfOptional = testOptional.orElse("Default String");
assertEquals("Default String", valueOfOptional);
}
Используйте его, когда хотите вернуть значение по умолчанию, если контейнер пустой.
objectOptional.orElse(null)
. Никогда так не делайте, и бейте по рукам тем, кто так делает.Метод orElseGet()
Метод похож на предыдущий, но вместо возвращения значения, он выполнит лямбду Supplier
.
@Test
@DisplayName("Используем метод orElseGet")
void useGetOrElseGet() {
final String testString = null;
final Optional<String> testOptional = Optional.ofNullable(testString);
final String valueOfOptional = testOptional.orElseGet(() -> {
System.out.println("String is null");
return "Default String";
});
assertEquals("Default String", valueOfOptional);
}
Используйте этот метод, когда вам не достаточно просто вернуть какое-то дефолтное значение, но и требуется выполнить какую-то более сложную логику.
Метод orElseThrow()
Вернет объект если он есть, в противном случае выбросит указанное исключение.
@Test
@DisplayName("Проверем метод orElseThrow")
void useOrElseThrow() {
final String testString = null;
final Optional<String> testOptional = Optional.ofNullable(testString);
assertThrows(
RuntimeException.class,
() -> testOptional.orElseThrow(() -> new RuntimeException("Null String in optional"))
);
}
Преобразование
Optional
также имеет ряд методов, которые могет быть знакомы вам по стримам: map
, filter
и flatMap
. Они имеют такие же названия, и делают примерно то же самое.
Метод filter()
Если внутри контейнера есть значение и оно удовлетворяет переданному условию в виде Predicate
лямбды, то будет возвращен новый объект Optional
с этим значением, иначе будет возвращен пустой Optional
.
@Test
@DisplayName("Проверка метода filter")
void useFilter() {
final String testString = "test string";
final Optional<String> testOptional = Optional.of(testString);
final Optional<String> filteredOptional = testOptional
.filter(ourTestSpring -> ourTestSpring.equals("test string"));
final String valueOfOptional = filteredOptional.get();
assertTrue(filteredOptional.isPresent());
assertEquals("test string", valueOfOptional);
}
Используйте его, когда вам нужен контейнер, объект в котором удвлетворят какому-то условию.
Метод map()
Если внутри контейнера есть значение, то к значению применяется переданная функция, результат помещается в новый Optional
и возвращается, в случае отсутствия значения будет возвращен пустой контейнер. Для преобразования используется Function
лямбда.
@Test
@DisplayName("Проверка метода map")
void useMap() {
final String testString = "test string";
final Optional<String> testOptional = Optional.of(testString);
final Optional<Integer> mappedOptional = testOptional
.map(ourTestSpring -> ourTestSpring.length());
final Integer lengthString = mappedOptional.get();
assertTrue(mappedOptional.isPresent());
assertEquals(testString.length(), lengthString);
}
Используйте этот метод, когда необходимо преобразовать объект внутри кинтейнера в другой объект.
Метод flatMap()
Как уже было сказано, map()
оборачивает возвращаймый результат лямбды Function. Но что, если эта лямбда у нас будет возвращать уже обернутый результат, у нас получится Optional<Optional<T>>
. С таким дважды упакованным объектом будет сложно работать.
Для примера, мы можем запрашивать какие-то данные о пользователе из другого сервиса, и этих данных тоже может не быть, поэтому возникает второй контейнер.
Optional<Optional<String>> optUserPhoneNumber = userRepository.findById(1L)
.map(user -> {
Optional<String> optPhoneNumber = phoneNumberRepository.findByLogin(user.getLogin());
return optPhoneNumber;
});
В таком случае используйте flatMap()
, он позволит вам избавиться от лишнего контейнера.
Optional<String> optUserPhoneNumber = userRepository.findById(1L)
.flatMap(user -> {
Optional<String> optPhoneNumber = phoneNumberRepository.findByLogin(user.getLogin());
return optPhoneNumber;
});
Метод stream()
В Java 9 в Optional
был добавлен новый метод stream()
, который позволяет удобно работать со стримом от коллекции Optional
элементов.
Допустим, вы запрашивали пользователей по идентификатору, а в ответ вам возвращался Optional<User>
. Вы сложили полученные объекты в коллекцию, и теперь с ними нужно как-то работать. Можно создать стрим от этой коллекции, и используя метод flatMap()
стрима и метод stream()
у Optional
, получить стрим существующих пользователей, все пустые контейнеры будут отброшены.
final List<Optional<User>> optUsers = new ArrayList<>();
final List<User> users = optUsers.stream()
.flatMap(itemOfArr -> itemOfArr.stream())
.map(user -> user.getLastName())
.toList();
Метод or()
Начиная с Java 11 добавили новый метод or()
. Он позволяет положить объект в пустой контейнер, раньше так сделать было нельзя.
@Test
@DisplayName("Проверка метода or")
void useOr() {
final String testString = null;
final Optional<String> testOptional = Optional.ofNullable(testString);
final Optional<String> optDefaultString = testOptional.or(() -> Optional.of("Default String"));
final String valueOfOptional = optDefaultString.get();
assertEquals("Default String", valueOfOptional);
}
Комбинирование методов
Все перечисленные методы возвращают в ответ Optional
, поэтому вы можете составлять из них цепочки, прямо как у стримов.
Пример оторванный от реальности, но иллюстрирующий цепочку методов:
final LocalDateTime now = LocalDateTime.now();
final DayOfWeek dayWeek = Optional.of(now)
.map(LocalDateTime::getDayOfWeek)
.filter(dayOfWeek -> DayOfWeek.SUNDAY.equals(dayOfWeek))
.orElse(DayOfWeek.MONDAY);
Прочие нюансы
Далее идут дополнительные советы и житейские мудрости, которые помогут вам еще лучше овладеть этим инструментом.
Когда стоит использовать Optional
Если открыть javadoc Optional
, можно найти там ответ на данный вопрос.
Optional
в первую очередь предназначен для использования в качестве типа возвращаемого значения метода, когда существует явная необходимость представлять «отсутствие результата» и где использование null
может вызвать ошибки.
Как НЕ стоит использовать Optional
А теперь разберемся основные ошибки при использовании Optional
.
Как параметр метода
Не стоит использовать Optional, в качестве параметра метода. Если пользователь метода с параметром Optional
не знает об этом, он может передать методу null
вместо Optional.empty()
. И обработка null
приведет к исключению NullPointerException
.
Как свойство класса
Также не используйте Optional
для объявления свойств класса.
Во-первых, у вас могут возникнуть проблемы с такими популярными фремворками, как Spring Data/Hibernate. Hibernate не может замапить значения из БД на Optional
напрямую , без кастомных конвертеров.
Хотя некоторые фреймворки, такие как Jackson отлично интегрируют Optional
в свою экосиситему. Как раз с Jackson можно подумать об использовании Optional
в качестве свойства класса, если создание объекта этого класса вы никика не контролируете. Например, при использовании Webhook.
Во-вторых, не стоит забывать, что Optional
это объект, который обычно нужен на пару секунд, после чего он может быть безболезненно удален сборщиком мусора. Но на создание объекта нужно время, и если мы храним Optional
в качестве поля, он может оставаться там вплоть до самой остановки программы. Вряд ли это приведет к проблемам в небольших приложениях, но все же учтите это.
В-третьих, использование таких полей будет неудобным Optional
не имплементирует интерфейс Serializable
. Проще говоря, любой объект, который содержит хотя бы одно Optional
поле, нельзя сериализовать. Хотя с приходом микросервисов платформенная сериализация не является настолько важной, как раньше.
Решением в такой ситуации может быть использование Optional
для геттеров класса. Однако у этого подхода есть один недостаток. Его нельзя полностью интегрировать с Lombok. Optional getters
не подерживаются библиотекой и, судя по некоторым обсуждениям на Github, не будут.
Как обертка коллекции
Не оборачивайте коллекции в Optional
. Любая коллекция является контейнером сама по себе. Чтобы вернуть пустую коллекцию, вместо null
, можно воспользоваться следующими методами Collections.emptyList()
, Collections.emptySet()
и прочими.
Optional не должен равняться null
Присваивание null
вместо объекта Optional
разрушает саму концепцию его использования. Никто из пользователей вашего метода не будет проверять Optional
на эквивалентность с null
. Вместо этого следует использовать Optional.empty()
.
Примитивы и Optional
Для работы с обертками примитивов есть java.util.OptionalDouble
, java.util.OptionalInt
и java.util.OptionalLong
, которые позволяют избегать лишних автоупаковок и распаковок. Однако не смотря на это, на практике используются они крайне редко.
Все эти классы похожи на Optional
, но не имеют методов преобразования. В них доступны только: get
, orElse
, orElseGet
, orElseThrow
, ifPresent
и isPresent
.
Резюмирую
Класс Optional
не решает проблему NullPointerException
полностью, но при правильном применении позволяет снизить количество ошибок, сделать код более читабельным и компактным. Использование Optional
не всегда уместно, но для возвращаемых значений из методов он подходит отлично.
Основные моменты, которые стоит запомнить
- Если методу требуется вернуть
null
, вернитеOptional
; - Не используйте
Optional
в качестве параметра методов и как свойство класса; - Проверяйте
Optional
перед тем, как достать его содержимое; - Никогда так не делайте:
objectOptional.orElse(null)
;