Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 OptionJava Optional
Some(value)Optional.of(value)
NoneOptional.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()

Когда использовать:

  • ✅ Возвращаемые значения методов
  • ✅ Цепочки преобразований
  • ❌ Параметры методов
  • ❌ Поля классов
  • ❌ Коллекции