Методы класса Optional в Java
Посмотрите на этот код и попробуйте угадать, что окажется в консоли:
public class OptionalDemo1 {
public static void main(String[] args) {
Optional<String> summary = Optional.of("Optional methods in Java");
System.out.println(summary.orElse(defaultSummary()));
}
static String defaultSummary() {
System.out.println("Готовлю значение по умолчанию...");
return "Default summary";
}
} Значение в Optional есть, значит defaultSummary() не нужен — но в консоли будут две строки: сначала «Готовлю значение по умолчанию…», и только потом Optional methods in Java. Аргумент orElse() — это обычное выражение, и Java вычисляет его до вызова метода, независимо от того, пуст контейнер или нет. Если заменить вызов на summary.orElseGet(OptionalDemo1::defaultSummary), лишней строки не будет.
Этот урок — продолжение темы «Что такое Optional», где разобраны назначение класса и способы создания контейнера (empty(), of(), ofNullable()). Здесь мы сразу переходим к методам: что делает каждый, что он возвращает и в какой ситуации его выбирать.
Все методы Optional<T> делятся на четыре группы:
- Создание —
empty(),of(),ofNullable(): статические фабрики, возвращаютOptional. - Проверка наличия значения —
isPresent(),isEmpty(),ifPresent(),ifPresentOrElse(). - Извлечение значения —
get(),orElse(),orElseGet(),orElseThrow(): возвращают сам объект типаT, а не обёртку. - Преобразование —
map(),flatMap(),filter(),or(),stream(): возвращают новыйOptional(илиStream) и потому складываются в цепочки.
1. Все методы Optional: сводная таблица
Класс java.util.Optional появился в Java 8, но с тех пор пополнялся почти в каждом крупном релизе. Подсвеченные строки — методы, которых нет в Java 8; если код должен собираться под старую версию, их использовать нельзя.
| Метод | Что делает | Когда использовать |
|---|---|---|
empty() | Возвращает пустой Optional | Когда значения заведомо нет: результат поиска, который ничего не нашёл |
of(T value) | Оборачивает значение; null вызывает NullPointerException | Когда значение точно не null |
ofNullable(T value) | Оборачивает значение, а для null возвращает пустой Optional | Когда значение может оказаться null (результат из БД, из Map) |
isPresent() | Возвращает true, если значение есть | В условии, когда нужна именно проверка, а не извлечение |
isEmpty() (Java 11+) | Возвращает true, если значения нет | Вместо !optional.isPresent() — читается лучше |
ifPresent(Consumer) | Выполняет действие, если значение есть; иначе не делает ничего | Вместо связки if (isPresent()) { get() } |
ifPresentOrElse(Consumer, Runnable) (Java 9+) | Действие для значения и отдельное действие для пустого контейнера | Когда нужны обе ветки: обработать значение или залогировать отсутствие |
get() | Возвращает значение или бросает NoSuchElementException | Только после явной проверки; в новом коде предпочтительнее orElseThrow() |
orElse(T other) | Возвращает значение или переданный объект | Когда значение по умолчанию уже готово: константа, литерал, пустой список |
orElseGet(Supplier) | Возвращает значение или результат функции, вычисленный лениво | Когда значение по умолчанию дорого получить: запрос, чтение файла, new |
orElseThrow() (Java 10+) | Возвращает значение или бросает NoSuchElementException | Замена get() с говорящим именем: видно, что метод может бросить исключение |
orElseThrow(Supplier) | Возвращает значение или бросает исключение, созданное функцией | Когда нужно своё исключение: UserNotFoundException, IllegalStateException |
filter(Predicate) | Оставляет значение, если оно проходит проверку, иначе отдаёт пустой Optional | Дополнительное условие внутри цепочки без if |
map(Function) | Применяет функцию к значению и заворачивает результат в Optional | Когда функция возвращает обычный объект: String, Integer |
flatMap(Function) | То же, но функция сама возвращает Optional, и лишняя обёртка не появляется | Цепочка методов, каждый из которых возвращает Optional |
or(Supplier<Optional>) (Java 9+) | Если пусто, подставляет Optional из другого источника | Цепочка запасных вариантов: кеш, затем файл, затем настройки |
stream() (Java 9+) | Превращает контейнер в Stream из нуля или одного элемента | Внутри flatMap(), чтобы отбросить пустые значения в потоке |
equals(Object) | Сравнивает содержимое двух контейнеров через equals() значений | В тестах и сравнении результатов |
hashCode() | Хеш значения или ноль для пустого контейнера | Отдельно почти не вызывается |
toString() | Даёт Optional[значение] или Optional.empty | Отладка и логи, но не вывод пользователю |
Полные сигнатуры и точные формулировки контрактов всегда можно свериться в официальном javadoc класса Optional.
2. Проверка значения: isPresent, isEmpty, ifPresent, ifPresentOrElse
isPresent() возвращает true, если контейнер не пуст. Это самый прямолинейный способ проверки — и самый близкий к обычному if (value != null).
Optional<String> summary = Optional.of("Optional methods in Java");
Optional<String> emptySummary = Optional.empty();
if (summary.isPresent()) {
System.out.println(summary.get()); // Optional methods in Java
}
System.out.println(summary.isPresent()); // true
System.out.println(emptySummary.isPresent()); // false С Java 11 есть обратный метод isEmpty(). Он не добавляет новых возможностей, но избавляет от отрицания, которое легко не заметить при чтении кода:
if (emptySummary.isEmpty()) { // вместо if (!emptySummary.isPresent())
System.out.println("Значения нет"); // Значения нет
} ifPresent(Consumer) выполняет переданное действие, только если значение есть. Для пустого контейнера не происходит ничего — ни исключения, ни вывода:
summary.ifPresent(System.out::println); // Optional methods in Java
emptySummary.ifPresent(System.out::println); // тишина, никакого NullPointerException Если нужна и вторая ветка — до Java 9 приходилось писать if/else вокруг isPresent(). Теперь есть ifPresentOrElse(Consumer, Runnable): первый аргумент получает значение, второй выполняется, когда значения нет.
summary.ifPresentOrElse(
s -> System.out.println("Найдено: " + s),
() -> System.out.println("Значение не задано"));
// Найдено: Optional methods in Java
emptySummary.ifPresentOrElse(
s -> System.out.println("Найдено: " + s),
() -> System.out.println("Значение не задано"));
// Значение не задано Обратите внимание на типы аргументов: Consumer<T> принимает значение, а Runnable ничего не принимает — принимать ему нечего, контейнер пуст.
3. Извлечение значения: get, orElse, orElseGet, orElseThrow
Все четыре метода возвращают объект типа T, то есть выводят вас из мира Optional обратно в обычный код. Разница — в том, что происходит, когда значения нет.
get()
get() отдаёт значение как есть, а на пустом контейнере бросает NoSuchElementException:
Optional<String> emptySummary = Optional.empty();
System.out.println(emptySummary.get());
// Exception in thread "main" java.util.NoSuchElementException: No value present Именно поэтому вызов get() без предварительной проверки бессмыслен: он превращает NullPointerException в NoSuchElementException, но не спасает от падения.
orElse(T other)
Возвращает значение, а если контейнер пуст — переданный объект:
Optional<String> summary = Optional.of("Optional methods in Java");
Optional<String> emptySummary = Optional.empty();
System.out.println(summary.orElse("Default summary")); // Optional methods in Java
System.out.println(emptySummary.orElse("Default summary")); // Default summary В качестве значения по умолчанию допустим и null: optional.orElse(null) — корректный способ вернуться к обычной ссылке, например при передаче результата в старый API.
orElseGet(Supplier)
То же самое, но значение по умолчанию описано функцией и вычисляется только в момент, когда оно действительно понадобилось:
System.out.println(emptySummary.orElseGet(() -> "Default summary")); // Default summary orElseThrow()
С Java 10 у Optional есть orElseThrow() без аргументов — он делает ровно то же, что get(), но по имени метода сразу видно, что вызов может завершиться исключением:
String value = summary.orElseThrow(); // Optional methods in Java
String fail = emptySummary.orElseThrow();
// java.util.NoSuchElementException: No value present orElseThrow(Supplier)
Версия с аргументом позволяет бросить своё исключение с осмысленным сообщением — это основной способ превратить «ничего не найдено» в ошибку бизнес-логики:
public User findUser(long id) {
return repository.findById(id) // Optional<User>
.orElseThrow(() -> new UserNotFoundException("Пользователь " + id + " не найден"));
} Важно
Метод get() не помечен как @Deprecated, поэтому компилятор на него не ругается. Но в javadoc, начиная с Java 10, прямо сказано: предпочтительная альтернатива — orElseThrow(). В новом коде используйте orElseThrow(), а get() оставьте для чтения старых проектов и вопросов на собеседовании.
4. orElse и orElseGet: в чём разница
Оба метода возвращают одно и то же значение, и в 99% случаев результат совпадает. Разница — в моменте вычисления запасного варианта:
orElse(T other)принимает готовый объект. Аргумент вычисляется всегда, ещё до входа в метод — даже еслиOptionalне пуст и результат будет отброшен.orElseGet(Supplier<T>)принимает функцию. Она вызывается лениво, только когда контейнер оказался пустым.
Пока справа от orElse() стоит литерал или константа, разницы нет. Она становится заметной, как только там оказывается вызов метода:
public class OptionalDemo2 {
public static void main(String[] args) {
Optional<String> summary = Optional.of("Optional methods in Java");
System.out.println("--- orElse ---");
System.out.println(summary.orElse(defaultSummary()));
// Готовлю значение по умолчанию...
// Optional methods in Java
System.out.println("--- orElseGet ---");
System.out.println(summary.orElseGet(OptionalDemo2::defaultSummary));
// Optional methods in Java
}
static String defaultSummary() {
System.out.println("Готовлю значение по умолчанию...");
return "Default summary";
}
} orElse() вызвал defaultSummary() и выбросил результат. Здесь это всего лишь лишняя строка в консоли, но на практике на этом месте бывает запрос в базу, обращение к внешнему сервису или создание тяжёлого объекта — и тогда программа делает работу, результат которой никому не нужен. Хуже, если метод меняет состояние: пишет в лог, увеличивает счётчик, создаёт запись — побочный эффект произойдёт и для непустого Optional.
Практическое правило: orElse() — для готовых значений (orElse(""), orElse(0), orElse(List.of())), orElseGet() — для всего, что нужно вычислять (orElseGet(this::loadDefaults), orElseGet(ArrayList::new)).
Вопрос с собеседования
«Чем orElse() отличается от orElseGet()?» — один из самых частых вопросов по Optional. Ответ на уровне «orElseGet принимает Supplier» засчитывают редко: ждут фразы про то, что аргумент orElse() вычисляется всегда, даже когда значение в контейнере есть, и что при дорогом или небезопасном вычислении это лишняя работа и незапланированные побочные эффекты.
5. Преобразование: map, flatMap, filter
Эти три метода возвращают не значение, а новый Optional, поэтому их можно соединять в цепочку и заканчивать её одним orElse() или orElseThrow().
map(Function)
map() применяет функцию к значению, если оно есть, и заворачивает результат обратно в Optional. Если контейнер пуст, функция не вызывается вообще:
Optional<String> summary = Optional.of("Optional methods in Java");
Optional<String> emptySummary = Optional.empty();
System.out.println(summary.map(s -> s.length()).orElse(0)); // 24
System.out.println(emptySummary.map(s -> s.length()).orElse(0)); // 0
Optional<Integer> length = summary.map(String::length);
System.out.println(length); // Optional[24] Тип меняется: из Optional<String> получился Optional<Integer>. И ещё одна важная деталь: если функция вернёт null, map() не упадёт, а отдаст пустой Optional — внутри используется ofNullable().
filter(Predicate)
filter() оставляет значение, если оно проходит проверку, и делает контейнер пустым, если не проходит:
Optional<String> summary = Optional.of("Optional methods in Java");
Optional<String> shortSummary = Optional.of("Java");
System.out.println(summary.filter(s -> s.length() > 10).orElse("Short summary"));
// Optional methods in Java
System.out.println(shortSummary.filter(s -> s.length() > 10).orElse("Short summary"));
// Short summary На пустом Optional предикат не вызывается — результат сразу пустой.
flatMap(Function)
flatMap() нужен, когда функция сама возвращает Optional. С обычным map() в таком случае получилась бы двойная обёртка Optional<Optional<T>>, из которой потом неудобно доставать значение:
// getAddress() возвращает Optional<Address>
Optional<Optional<Address>> wrapped = user.map(User::getAddress); // двойная обёртка
Optional<Address> address = user.flatMap(User::getAddress); // то, что нужно Именно на flatMap() держатся цепочки вложенных вызовов, которые раньше писались лесенкой из проверок на null:
String city = repository.findById(42) // Optional<User>
.flatMap(User::getAddress) // Optional<Address>, метод возвращает Optional
.map(Address::getCity) // Optional<String>, метод возвращает String
.filter(c -> !c.isBlank())
.orElse("город не указан"); Правило выбора простое: если метод возвращает обычный объект — map(); если метод уже возвращает Optional — flatMap().
6. or() и stream()
Оба метода появились в Java 9 и закрывают сценарии, для которых раньше приходилось писать вспомогательный код.
or(Supplier<Optional>)
or() отличается от orElseGet() тем, что возвращает не значение, а Optional — поэтому вызовы можно ставить один за другим:
Optional<String> empty = Optional.empty();
System.out.println(empty.or(() -> Optional.of("fallback"))); // Optional[fallback]
System.out.println(Optional.of("value").or(() -> Optional.of("fallback"))); // Optional[value] Типичное применение — цепочка источников: взять настройку из кеша, если там пусто — из файла, если и там пусто — из переменных окружения:
String mode = readFromCache() // Optional<String>
.or(this::readFromFile) // Optional<String>
.or(this::readFromEnvironment) // Optional<String>
.orElse("default"); Как и orElseGet(), or() ленив: следующий источник не читается, если предыдущий уже дал значение.
stream()
stream() превращает контейнер в поток из одного элемента — или в пустой поток, если значения нет. Пользы в одиночном вызове немного, но в связке с flatMap() метод убирает целый шаблон:
List<Optional<String>> results = List.of(
Optional.of("Java"), Optional.empty(), Optional.of("Optional"));
List<String> values = results.stream()
.flatMap(Optional::stream)
.toList(); // [Java, Optional]
// до Java 9 то же самое писали в два шага:
List<String> old = results.stream()
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList()); 7. OptionalInt, OptionalLong, OptionalDouble
Для примитивов существуют отдельные классы — OptionalInt, OptionalLong и OptionalDouble. Они избавляют от упаковки в объекты-обёртки и возвращаются из методов примитивных потоков (average(), max(), findFirst()).
OptionalInt optionalInt = OptionalInt.of(1);
System.out.println(optionalInt); // OptionalInt[1]
System.out.println(optionalInt.getAsInt()); // 1
System.out.println(OptionalInt.empty().orElse(0)); // 0
OptionalDouble average = IntStream.of(28, 4, 91, 30).average();
System.out.println(average); // OptionalDouble[38.25]
System.out.println(average.getAsDouble()); // 38.25
OptionalLong optionalLong = OptionalLong.of(10L);
optionalLong.ifPresent(value -> System.out.println(value)); // 10 Вместо get() здесь getAsInt(), getAsLong() и getAsDouble() — они возвращают примитив. Набор методов беднее, чем у Optional<T>: есть isPresent(), isEmpty(), ifPresent(), ifPresentOrElse(), orElse(), orElseGet(), orElseThrow() и stream(), но нет map(), filter(), flatMap() и or(). Если нужны преобразования, перейдите к объектной версии: OptionalInt.of(1).stream().boxed().findFirst() вернёт Optional<Integer>, у которого эти методы есть.
8. equals(), hashCode() и toString()
Optional переопределяет все три метода Object. equals() сравнивает не ссылки на контейнеры, а их содержимое:
System.out.println(Optional.of("Java").equals(Optional.of("Java"))); // true
System.out.println(Optional.empty().equals(Optional.empty())); // true
System.out.println(Optional.of("Java").equals(Optional.empty())); // false
System.out.println(Optional.of("Java").equals("Java")); // false Последняя строка важна: контейнер никогда не равен своему содержимому. hashCode() возвращает хеш значения, а для пустого контейнера — 0.
toString() даёт удобное для отладки представление, но не годится для вывода пользователю:
System.out.println(Optional.of("Java")); // Optional[Java]
System.out.println(Optional.empty()); // Optional.empty Если в консоли или в логе появилось Optional[38.25] вместо числа, значит где-то забыли распаковать контейнер через orElse(), orElseThrow() или getAsDouble().
9. Неочевидные моменты
- Аргумент
orElse()вычисляется всегда. Даже когда значение в контейнере есть. Для дорогих вычислений и методов с побочными эффектами используйтеorElseGet(). get()без проверки — замаскированныйNullPointerException. Вместо него беритеorElse(),orElseGet()илиorElseThrow()со своим сообщением.Optionalкак поле класса и как параметр метода — антипаттерн. Класс задумывался как тип возвращаемого значения метода, который может ничего не найти. В полях он добавляет лишний объект на каждый экземпляр и ломает сериализацию:Optionalне реализуетSerializable. В параметрах он вынуждает вызывающий код писатьOptional.of(x)и допускает третье состояние —nullвместо самогоOptional. Для параметров используйте перегрузку методов, для полей — обычное поле и геттер, возвращающийOptional.- Связка
isPresent()+get()ничем не лучше проверки наnull. Она просто длиннее. Идиоматичный код обходитсяifPresent(),map(),filter()иorElse(). map()проглатываетnull. Если функция внутриmap()вернулаnull, исключения не будет — получится пустойOptional. Иногда это удобно, но бывает и источником незаметно потерянных значений.- Не храните
Optionalв коллекциях.List<Optional<String>>— почти всегда признак того, что пустые элементы стоило отбросить раньше, например черезflatMap(Optional::stream). Optionalне защищает отnullсам по себе. Переменная типаOptional<String>тоже может бытьnull, если кто-то присвоил ейnull. Метод, возвращающийOptional, обязан возвращатьOptional.empty(), а неnull.
Совет
Хороший признак того, что Optional используется по назначению: контейнер создаётся внутри метода, живёт несколько строк и исчезает на orElse() или ifPresent(). Если Optional уходит в поле объекта, в параметр конструктора или в коллекцию — почти наверняка проще вернуться к обычной ссылке и проверке.
Часто задаваемые вопросы
Чем orElse отличается от orElseGet?
Результат у них одинаковый, разное время вычисления запасного значения. orElse принимает готовый объект, и это выражение вычисляется всегда, даже если Optional не пуст и результат будет отброшен. orElseGet принимает Supplier и вызывает его только тогда, когда контейнер пуст. Если справа стоит литерал или константа, разницы нет; если там вызов метода, запрос в базу или создание объекта, выбирайте orElseGet - иначе программа делает ненужную работу и может выполнить побочный эффект вроде записи в лог.
Почему get() бросает NoSuchElementException и чем его заменить?
Пустой Optional не содержит значения, поэтому get() нечего вернуть, и он бросает NoSuchElementException с сообщением No value present. Начиная с Java 10 предпочтительной альтернативой в javadoc названа orElseThrow - тот же результат, но по имени метода видно, что вызов может завершиться исключением. Если падать нельзя, используйте orElse, orElseGet или orElseThrow с собственным исключением.
Можно ли использовать Optional как поле класса или параметр метода?
Не рекомендуется. Optional спроектирован как тип возвращаемого значения метода, который может ничего не найти. В поле он добавляет лишний объект на каждый экземпляр и ломает сериализацию, потому что Optional не реализует Serializable. В параметре он заставляет вызывающий код оборачивать аргумент и допускает третье состояние, когда в параметр передали null вместо самого Optional. Для необязательных параметров используйте перегрузку методов, а для полей храните обычную ссылку и возвращайте Optional из геттера.
Чем map отличается от flatMap в Optional?
map применяет функцию к значению и сам оборачивает результат в Optional. flatMap применяет функцию, которая уже возвращает Optional, и не добавляет вторую обёртку. Если вызвать map для метода, возвращающего Optional, получится Optional внутри Optional, из которого неудобно доставать значение. Правило простое: метод возвращает обычный объект - map, метод возвращает Optional - flatMap.
Как проверить, что Optional пуст, не используя отрицание isPresent?
С Java 11 есть метод isEmpty, который возвращает true для пустого контейнера: вместо условия с отрицанием перед isPresent пишется условие с вызовом isEmpty. Если нужно выполнить действие и для значения, и для его отсутствия, с Java 9 доступен ifPresentOrElse: первый аргумент типа Consumer получает значение, второй типа Runnable выполняется, когда значения нет.
Комментарии