2.x. optional
optional
Материалы
Optional<T> — контейнер, который может содержать или не содержать значение. Введён в Java 8 как способ явно выразить отсутствие результата без использования null.
Зачем нужен Optional
Проблема null — одна из самых распространённых причин ошибок в Java:
// Опасный код — NullPointerException подстерегает
String city = user.getAddress().getCity().toUpperCase();
// Защитное программирование — громоздко
String city = null;
if (user != null) {
Address address = user.getAddress();
if (address != null) {
String c = address.getCity();
if (c != null) {
city = c.toUpperCase();
}
}
}
Optional решает эту проблему, делая возможное отсутствие значения явным на уровне типа:
// Сигнатура метода явно говорит: результата может не быть
Optional<User> findUserById(long id);
// Компилятор заставляет обработать оба случая
String city = findUserById(42)
.flatMap(User::getAddress)
.map(Address::getCity)
.map(String::toUpperCase)
.orElse("Unknown");
Создание Optional
Optional.empty() — пустой контейнер
Optional<String> empty = Optional.empty();
System.out.println(empty.isPresent()); // false
System.out.println(empty.isEmpty()); // true (Java 11+)
Optional.of(value) — значение гарантированно не null
Optional<String> name = Optional.of("Alice");
// NullPointerException! of() не принимает null
Optional<String> oops = Optional.of(null); // Бросит исключение
Используйте of() когда уверены, что значение не null. Это документирует намерение и упадёт сразу, если предположение нарушено.
Optional.ofNullable(value) — значение может быть null
String possiblyNull = getUserInput();
Optional<String> maybe = Optional.ofNullable(possiblyNull);
// Если possiblyNull == null, получим Optional.empty()
// Иначе — Optional со значением
Используйте ofNullable() при работе с внешними данными или legacy-кодом.
Проверка наличия значения
Optional<String> opt = Optional.of("Hello");
// isPresent() — есть ли значение
if (opt.isPresent()) {
System.out.println("Значение: " + opt.get());
}
// isEmpty() — пуст ли контейнер (Java 11+)
if (opt.isEmpty()) {
System.out.println("Пусто");
}
Важно: Избегайте
isPresent()+get()— это возврат к проверкам наnull. Используйте функциональные методы.
Получение значения
get() — небезопасно!
Optional<String> opt = Optional.empty();
String value = opt.get(); // NoSuchElementException!
Предупреждение:
get()бросает исключение для пустого Optional. ПредпочитайтеorElseThrow()— он явно выражает намерение.
orElse(defaultValue) — значение по умолчанию
String name = Optional.ofNullable(userName)
.orElse("Anonymous");
// Если userName == null, вернёт "Anonymous"
Особенность: значение по умолчанию вычисляется всегда:
String name = Optional.of("Alice")
.orElse(computeExpensiveDefault()); // computeExpensiveDefault() вызовется!
orElseGet(supplier) — ленивое значение по умолчанию
String name = Optional.ofNullable(userName)
.orElseGet(() -> computeExpensiveDefault());
// computeExpensiveDefault() вызовется ТОЛЬКО если userName == null
Используйте orElseGet() когда вычисление значения по умолчанию дорогое.
orElseThrow() — исключение если пусто
// NoSuchElementException с понятным сообщением (Java 10+)
User user = findUserById(id).orElseThrow();
// Своё исключение
User user = findUserById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
// Ссылка на конструктор
User user = findUserById(id)
.orElseThrow(UserNotFoundException::new);
Условные действия
ifPresent(consumer) — выполнить если есть значение
Optional<User> user = findUserById(42);
// Вместо if (user.isPresent()) { ... }
user.ifPresent(u -> System.out.println("Found: " + u.getName()));
// Ссылка на метод
user.ifPresent(System.out::println);
ifPresentOrElse(consumer, runnable) — обработать оба случая (Java 9+)
findUserById(42).ifPresentOrElse(
user -> System.out.println("Found: " + user.getName()),
() -> System.out.println("User not found")
);
Трансформации
map(function) — преобразовать значение
Optional<String> name = Optional.of("alice");
Optional<String> upperName = name.map(String::toUpperCase);
// Optional["ALICE"]
Optional<Integer> length = name.map(String::length);
// Optional[5]
// Если Optional пуст, map возвращает пустой Optional
Optional<String> empty = Optional.<String>empty().map(String::toUpperCase);
// Optional.empty()
Важно: Если функция возвращает null, результат — пустой Optional:
Optional<String> result = Optional.of("test")
.map(s -> null); // Optional.empty()
flatMap(function) — для вложенных Optional
Когда функция сама возвращает Optional, используйте flatMap() чтобы избежать Optional<Optional<T>>:
class User {
Optional<Address> getAddress() { ... }
}
class Address {
Optional<String> getCity() { ... }
}
// map создал бы Optional<Optional<Address>>
Optional<User> user = findUserById(42);
// flatMap "разворачивает" вложенный Optional
Optional<String> city = user
.flatMap(User::getAddress)
.flatMap(Address::getCity);
Сравнение map vs flatMap:
Optional<String> opt = Optional.of("hello");
// Функция возвращает обычное значение → map
Optional<Integer> length = opt.map(String::length);
// Функция возвращает Optional → flatMap
Optional<Character> firstChar = opt.flatMap(s ->
s.isEmpty() ? Optional.empty() : Optional.of(s.charAt(0))
);
filter(predicate) — фильтрация по условию
Optional<String> name = Optional.of("Alice");
Optional<String> longName = name.filter(n -> n.length() > 3);
// Optional["Alice"]
Optional<String> shortName = name.filter(n -> n.length() > 10);
// Optional.empty()
Пример использования:
// Найти пользователя, только если он активен
Optional<User> activeUser = findUserById(42)
.filter(User::isActive);
Комбинирование Optional
or(supplier) — альтернативный Optional (Java 9+)
Optional<String> primary = Optional.empty();
Optional<String> fallback = Optional.of("fallback");
Optional<String> result = primary.or(() -> fallback);
// Optional["fallback"]
// Цепочка альтернатив
Optional<User> user = findInCache(id)
.or(() -> findInDatabase(id))
.or(() -> findInRemoteService(id));
Отличие от orElseGet(): or() возвращает Optional, а не значение.
Интеграция со Stream
stream() — Optional как Stream (Java 9+)
Optional<String> opt = Optional.of("hello");
Stream<String> stream = opt.stream();
// Stream с одним элементом или пустой Stream
Главное применение — фильтрация пустых Optional в потоке:
List<Optional<String>> optionals = List.of(
Optional.of("a"),
Optional.empty(),
Optional.of("b"),
Optional.empty()
);
// Извлечь только присутствующие значения
List<String> values = optionals.stream()
.flatMap(Optional::stream)
.toList();
// ["a", "b"]
// До Java 9 приходилось писать так:
List<String> valuesBefore9 = optionals.stream()
.filter(Optional::isPresent)
.map(Optional::get)
.toList();
Примитивные версии
Для избежания boxing существуют специализированные классы:
OptionalInt optInt = OptionalInt.of(42);
OptionalLong optLong = OptionalLong.of(1_000_000_000L);
OptionalDouble optDouble = OptionalDouble.of(3.14);
// Получение значения
int value = optInt.orElse(0);
int value2 = optInt.orElseThrow();
// Проверка
if (optInt.isPresent()) {
optInt.ifPresent(System.out::println);
}
// Создание пустых
OptionalInt empty = OptionalInt.empty();
Ограничение: Примитивные Optional не имеют методов
map(),flatMap(),filter(),or(). Они проще, но менее функциональны.
// Для трансформаций нужно конвертировать
OptionalInt optInt = OptionalInt.of(42);
Optional<String> asString = optInt.isPresent()
? Optional.of(String.valueOf(optInt.getAsInt()))
: Optional.empty();
// Или через stream (Java 9+)
Optional<String> asString2 = optInt.stream()
.mapToObj(String::valueOf)
.findFirst();
Цепочки преобразований
Optional раскрывает свою мощь в цепочках:
record User(String name, Address address) {}
record Address(String city, String street) {}
Optional<User> user = findUserById(42);
// Безопасная навигация по вложенным объектам
String street = user
.map(User::address) // Optional<Address>
.map(Address::street) // Optional<String>
.map(String::toUpperCase) // Optional<String>
.orElse("UNKNOWN");
// С Optional-полями используйте flatMap
record UserV2(String name, Optional<Address> address) {}
String city = findUserByIdV2(42)
.flatMap(UserV2::address) // flatMap для Optional<Optional<Address>> → Optional<Address>
.map(Address::city)
.filter(c -> !c.isBlank())
.orElse("Unknown");
Практический пример
public class OrderService {
private final UserRepository userRepo;
private final OrderRepository orderRepo;
private final DiscountService discountService;
/**
* Вычисляет финальную цену заказа с учётом скидки пользователя.
* @return финальная цена или empty, если заказ не найден
*/
public Optional<BigDecimal> calculateFinalPrice(long orderId) {
return orderRepo.findById(orderId)
.map(order -> {
BigDecimal basePrice = order.getTotalPrice();
// Получить скидку пользователя (может не быть)
BigDecimal discount = userRepo.findById(order.getUserId())
.flatMap(discountService::getDiscount)
.orElse(BigDecimal.ZERO);
return basePrice.subtract(
basePrice.multiply(discount)
);
});
}
/**
* Найти последний заказ пользователя определённой категории.
*/
public Optional<Order> findLastOrderInCategory(long userId, Category category) {
return userRepo.findById(userId)
.flatMap(user -> orderRepo.findLastByUserId(user.getId()))
.filter(order -> order.getCategory() == category);
}
}
Лучшие практики
✅ Используйте Optional для возвращаемых значений
// Хорошо: явно показывает, что результата может не быть
public Optional<User> findUserById(long id) {
User user = database.query(id);
return Optional.ofNullable(user);
}
❌ Не используйте Optional для параметров методов
// Плохо: усложняет API
public void processUser(Optional<User> user) { ... }
// Хорошо: перегрузка или @Nullable
public void processUser(User user) { ... }
public void processUser() { ... } // без пользователя
❌ Не используйте Optional для полей класса
// Плохо: Optional не Serializable, накладные расходы
class User {
private Optional<String> middleName; // Не делайте так
}
// Хорошо: nullable поле или пустая строка
class User {
private String middleName; // null означает отсутствие
public Optional<String> getMiddleName() {
return Optional.ofNullable(middleName);
}
}
❌ Не используйте Optional в коллекциях
// Плохо
List<Optional<User>> users;
// Хорошо: просто фильтруйте null
List<User> users; // без null-элементов
❌ Не используйте get() без проверки
// Плохо — эквивалентно работе с null
if (optional.isPresent()) {
return optional.get();
}
// Хорошо
return optional.orElse(defaultValue);
return optional.orElseThrow(() -> new NotFoundException());
✅ Предпочитайте функциональный стиль
// Плохо — императивный стиль
Optional<User> opt = findUser();
String name;
if (opt.isPresent()) {
name = opt.get().getName().toUpperCase();
} else {
name = "ANONYMOUS";
}
// Хорошо — функциональный стиль
String name = findUser()
.map(User::getName)
.map(String::toUpperCase)
.orElse("ANONYMOUS");
✅ orElseGet() для дорогих вычислений
// Плохо: createDefaultUser() вызывается всегда
user.orElse(createDefaultUser());
// Хорошо: вызывается только при необходимости
user.orElseGet(() -> createDefaultUser());
user.orElseGet(this::createDefaultUser);
Антипаттерны
// ❌ Optional.of(null)
Optional.of(possiblyNullValue); // NPE!
// ✅ Optional.ofNullable(possiblyNullValue);
// ❌ Сравнение с null
if (optional == null) { ... }
// ✅ Optional никогда не должен быть null
if (optional.isEmpty()) { ... }
// ❌ Вложенный Optional
Optional<Optional<String>> nested;
// ✅ Используйте flatMap
// ❌ optional.get() без проверки
return optional.get();
// ✅ return optional.orElseThrow();
// ❌ isPresent + get
if (opt.isPresent()) return opt.get();
// ✅ return opt.orElse(default);
// ❌ Возврат null вместо Optional.empty()
public Optional<User> find() {
if (notFound) return null; // Никогда!
}
// ✅ return Optional.empty();
Сравнение с Rust Option
Для знакомых с Rust — соответствие API:
| Rust Option | Java Optional |
|---|---|
Some(value) | Optional.of(value) |
None | Optional.empty() |
is_some() | isPresent() |
is_none() | isEmpty() |
unwrap() | get() / orElseThrow() |
unwrap_or(default) | orElse(default) |
unwrap_or_else(f) | orElseGet(f) |
map(f) | map(f) |
and_then(f) | flatMap(f) |
filter(p) | filter(p) |
or(other) | or(supplier) |
or_else(f) | or(f) |
Ключевое отличие: в Rust Option — это enum, pattern matching встроен в язык. В Java используется method chaining.
Резюме
Optional — это:
- Контейнер для значения, которое может отсутствовать
- Замена
nullдля возвращаемых значений - Инструмент для явного выражения намерений в API
Основные методы:
| Создание | empty(), of(v), ofNullable(v) |
|---|---|
| Проверка | isPresent(), isEmpty() |
| Извлечение | orElse(), orElseGet(), orElseThrow() |
| Действия | ifPresent(), ifPresentOrElse() |
| Преобразование | map(), flatMap(), filter() |
| Комбинирование | or(), stream() |
Когда использовать:
- ✅ Возвращаемые значения методов
- ✅ Цепочки преобразований
- ❌ Параметры методов
- ❌ Поля классов
- ❌ Коллекции