Урок 8 из 11 базовый около 25 мин

Optional: честная работа с отсутствующим значением

Когда возвращать Optional, как раскрывать его без get() и почему он не подходит для полей и параметров.

Чем плох null как ответ

Метод, который иногда возвращает null, перекладывает проверку на вызывающий код и ничего не говорит об этом в сигнатуре. Забытая проверка обнаруживается только в рантайме как NullPointerException, часто далеко от места, где значение потерялось. Optional делает возможность отсутствия частью типа: Optional<User> честно сообщает, что пользователя может не быть.

import java.util.Optional;

record User(String login, String email) {}

static Optional<User> findByLogin(java.util.List<User> users, String login) {
    return users.stream()
            .filter(u -> u.login().equalsIgnoreCase(login))
            .findFirst();
}

Как раскрывать Optional

Метод get() бросает исключение на пустом значении, поэтому его использование почти всегда ошибка: это тот же null, только с другим именем исключения. Вместо него есть выразительные способы:

var users = java.util.List.of(new User("anna", "anna@example.org"));
Optional<User> found = findByLogin(users, "ANNA");

// Значение по умолчанию
String email = found.map(User::email).orElse("нет адреса");

// Ленивое значение по умолчанию: вычисляется только при отсутствии
String email2 = found.map(User::email).orElseGet(() -> buildFallbackEmail());

// Исключение с осмысленным сообщением, если отсутствие — действительно ошибка
User user = found.orElseThrow(() -> new IllegalArgumentException("Пользователь не найден"));

// Действие только при наличии, иначе другое действие
found.ifPresentOrElse(
        u -> System.out.println("Найден " + u.login()),
        () -> System.out.println("Никого нет"));

Разница между orElse и orElseGet важна: аргумент orElse вычисляется всегда, даже когда значение есть. Для дорогих операций и обращений к базе используйте orElseGet.

Цепочки: map, flatMap, filter

Optional ведёт себя как поток из одного или нуля элементов, поэтому преобразования выстраиваются в цепочку без единой проверки на null:

record Address(String city) {}
record Person(String name, Optional<Address> address) {}

static String cityOf(Person person) {
    return person.address()
            .map(Address::city)
            .filter(city -> !city.isBlank())
            .map(String::toUpperCase)
            .orElse("город не указан");
}

Если функция внутри map сама возвращает Optional, получится Optional<Optional<T>>. Чтобы этого не случилось, используйте flatMap:

static Optional<String> primaryEmail(User user) { /* ... */ return Optional.empty(); }

Optional<String> primary = findByLogin(users, "anna").flatMap(u -> primaryEmail(u));

Метод or позволяет подставить другой Optional, если первый пуст: findInCache(id).or(() -> findInDatabase(id)).

Где Optional уместен, а где нет

МестоРекомендацияПочему
Возвращаемое значение методадаДля этого он и создан: отсутствие результата видно в сигнатуре.
Поле класса или записинежелательноКласс не сериализуется стандартными средствами, а поле всё равно может оказаться null. Держите обычный тип и проверяйте в конструкторе.
Параметр методанетВызывающему приходится оборачивать значения; перегрузка метода или значение по умолчанию читаются проще.
Элемент коллекциинетПустой список уже выражает отсутствие; List<Optional<T>> только мешает.
Возврат коллекциинетВерните пустую коллекцию, а не Optional<List>.

Пример с записью Person выше нарушает второе правило намеренно, чтобы показать flatMap; в рабочем коде храните Address address и превращайте его в Optional.ofNullable(address) в методе доступа.

Создание Optional

  • Optional.of(value) — значение точно не null, иначе исключение сразу.
  • Optional.ofNullable(value) — обёртка над результатом старого API, который может вернуть null.
  • Optional.empty() — явное «ничего».

Для примитивов есть OptionalInt, OptionalLong и OptionalDouble; их возвращают числовые потоки, например IntStream.max().

Частая ошибка

Писать if (opt.isPresent()) { var v = opt.get(); ... }. Это тот же код с проверкой на null, только длиннее. Замените на ifPresent, map или orElseThrow: они выражают намерение и не дают забыть про пустой случай.

Пример: настройка с запасным значением

static Optional<String> env(String name) {
    return Optional.ofNullable(System.getenv(name)).filter(v -> !v.isBlank());
}

int port = env("APP_PORT")
        .map(Integer::parseInt)
        .filter(p -> p > 0 && p < 65536)
        .orElse(8080);
System.out.println("Порт: " + port);

Переменная окружения может отсутствовать, быть пустой или содержать мусор — цепочка обрабатывает всё это без единой проверки на null. Заметьте, что Integer.parseInt бросит исключение на нечисловом значении: если это допустимая ситуация, оберните разбор в метод, возвращающий Optional<Integer>.

Попробуйте сами

  1. Напишите метод Optional<Integer> parseInt(String s), который возвращает пустое значение вместо исключения, и используйте его в примере с портом.
  2. Перепишите Person так, чтобы поле было обычным Address (возможно, null), а метод address() возвращал Optional. Убедитесь, что cityOf работает без изменений.
  3. Найдите в своём коде три места с проверкой != null после вызова метода и подумайте, стал бы код яснее с Optional в сигнатуре. Не все три обязаны переписываться.