сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Stream API

Optional и работа с null

Optional: отсутствие значения, записанное в типе

Поставил счётчик вызовов на дорогую функцию и позвал её как значение по умолчанию для НЕПУСТОГО Optional. orElse(expensive()) вызвал её один раз, хотя значение было на месте и умолчание не понадобилось. orElseGet(this::expensive) не вызвал ни разу.

Это самый частый способ уронить производительность на ровном месте: «почему у нас запрос в базу на каждый вызов, значение же есть». А сам Optional спрашивают, чтобы понять - ты усвоил идею или оборачиваешь всё подряд.

// Формулировки: «зачем Optional, если можно вернуть null?», «orElse или orElseGet?», «почему Optional не советуют в полях?».

Зачем он нужен и где вреден

Место Optional - возвращаемые значения методов. Сигнатура Optional<User> findById(id) прямо говорит: ответа может не быть, реши что делать. Обычный null того же не даёт - по сигнатуре User findById(id) не видно ничего, проверку легко забыть, и NullPointerException выстрелит позже и в другом месте.

Границы применения. В полях класса Optional не держат: он ломает сериализацию (превращение объекта в байты для хранения или передачи) и не дружит с JPA (Java Persistence API, стандарт работы с базой через объекты). В параметрах тоже не держат - вызывающему придётся заворачивать значение; лучше объявить два метода с одним именем и разным набором параметров, это называется перегрузкой. И для коллекций он лишний: пустой список УЖЕ означает «ничего нет», а Optional<List> получается обёрткой поверх обёртки.

// Вход из мира, где null допустим - Optional.ofNullable(x): я проверил, ofNullable(null) спокойно даёт Optional.empty, а вот Optional.of(null) сразу бросает NullPointerException. И дисциплина: сама переменная типа Optional никогда не должна быть null, иначе вся затея теряет смысл.

Optional
контейнер, который делает возможное отсутствие значения частью типа
ofNullable
безопасная упаковка значения, которое может оказаться null

Цепочки вместо лестницы проверок

Сила Optional в цепочках: user.map(User::getAddress).map(Address::getCity).orElse("нет данных") проходит по вложенным полям и не падает, если по дороге чего-то нет. Лестница из вложенных if исчезает.

Когда функция сама возвращает Optional, нужен flatMap, иначе выйдет матрёшка. Я проверил на живом коде: map(Optional::of) дал Optional[Optional[Казань]], а flatMap(Optional::of) - Optional[Казань].

Связка isPresent() плюс get() - та же проверка на null, только переодетая. К тому же get() на пустом бросает NoSuchElementException с текстом No value present. Вместо этого есть идиомы, которые сразу выражают намерение: ifPresent(действие), ifPresentOrElse(есть, нет), orElseThrow(() -> new NotFoundException(id)) для случая «обязано быть». А метод stream() превращает Optional в стрим из нуля или одного элемента: список Optional-ов через flatMap(Optional::stream) у меня свернулся в список только присутствующих значений.

map / flatMap у Optional
преобразование / когда функция сама возвращает Optional
orElseThrow
распаковать или бросить осмысленное исключение

orElse против orElseGet

Вернёмся к замеру из начала. orElse принимает готовое ЗНАЧЕНИЕ, а значит аргумент вычисляется до вызова метода - всегда, независимо от того, пустой Optional или нет. Это обычные правила Java, никакой магии: чтобы передать результат expensive(), его сперва надо получить.

orElseGet принимает поставщика - объект, который умеет вычислить значение, но пока не вычислял. Optional зовёт его только когда внутри пусто. Мои три замера: значение есть плюс orElse - функция вызвана 1 раз; значение есть плюс orElseGet - 0 раз; пусто плюс orElseGet - 1 раз.

Правило простое. Умолчание-константа - orElse, разницы нет. Умолчание, которое надо вычислить: запрос в базу, создание объекта, чтение файла, запись в лог - только orElseGet.

var present = Optional.of("значение есть");

present.orElse(expensive());        // expensive() вызвана 1 раз
present.orElseGet(S4::expensive);   // вызвана 0 раз
Optional.empty().orElseGet(S4::expensive);  // вызвана 1 раз
orElse / orElseGet
готовое значение, считается всегда / поставщик, зовут только при пустом

Как отвечать: «Зачем Optional, если можно вернуть null?»

Потому что null - неявный договор. По сигнатуре User findById(id) не видно, что ответа может не быть; проверку легко забыть, и NullPointerException выстрелит позже и в другом месте программы. Optional<User> делает отсутствие частью типа: вызывающий обязан распаковать значение и явно выбрать - подставить умолчание, бросить исключение, пропустить. Плюс цепочки map и flatMap проходят по вложенным полям без лестницы проверок. При этом я держу границы: Optional - для возвращаемых значений, в поля его не кладу, потому что страдают JPA и сериализация, в параметры тоже не кладу, лучше перегрузки, а вместо Optional со списком возвращаю пустой список. И слежу за orElse: он вычисляет умолчание всегда, даже когда значение есть, я это проверял счётчиком вызовов. Где умолчание дорогое - только orElseGet.

Почему это сильный ответ: противопоставлены неявный и явный договоры, названы границы применимости и снята иллюзия, будто Optional сам по себе спасает от NullPointerException.

На чём валят

  • Звать get() без проверки. На пустом это NoSuchElementException с текстом No value present; вместо него есть orElseThrow с понятным исключением.
  • orElse с дорогим вызовом внутри. Он выполнится даже когда значение на месте - я ловил лишний вызов счётчиком; ленивый вариант это orElseGet.
  • Optional в полях сущностей и объектов передачи данных. Ломает JPA и сериализацию; поле оставляют обычным, а Optional отдаёт геттер.
  • Optional<List<T>> - обёртка поверх обёртки. Пустой список сам по себе означает «ничего нет».
  • Конструкция if (opt.isPresent()) { opt.get() }. Это проверка на null в новом костюме, то же намерение выражают map и ifPresent.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.

  1. #optional_null1 / 5
    Почему Optional.get() без проверки считается плохой практикой?
    A)Get() возвращает null для пустого Optional, из-за чего снова возникает та же проблема с NPE
    B)Get() изменяет состояние Optional, делая его пустым после первого вызова, поэтому повторно не работает
    C)Бросит NoSuchElementException, если Optional пуст — теряется смысл Optional
    D)Get() медленно работает, так как каждый раз заново разворачивает обёртку через тяжёлую рефлексию
    показать ответ и разбор
    +C)Бросит NoSuchElementException, если Optional пуст — теряется смысл Optional

    // разбор: get() возвращает значение, но на ПУСТОМ Optional бросает NoSuchElementException. Вызывать его без предварительной проверки isPresent() — по сути тот же необработанный «null», ради ухода от которого Optional и придуман. Идиоматично вместо get() использовать безопасные методы: map/flatMap/filter (преобразования), orElse/orElseGet (значение по умолчанию), orElseThrow (осмысленное исключение), ifPresent/ifPresentOrElse (действие). isPresent()+get() — это «замаскированная null-проверка», обычно её переписывают функционально.

  2. #optional_null2 / 5
    Чем flatMap отличается от map у Optional?
    A)Map применяется к пустому Optional, а flatMap — только к непустому, в этом их различие
    B)FlatMap разворачивает значение в null, а map оставляет его в обёртке, поэтому flatMap опаснее
    C)Map и flatMap идентичны для Optional; flatMap оставлен лишь для совместимости со стримами
    D)flatMap для функции, возвращающей Optional (иначе вложенность Optional<Optional<T>>)
    показать ответ и разбор
    +D)flatMap для функции, возвращающей Optional (иначе вложенность Optional<Optional<T>>)

    // разбор: map(f) применяет f к значению и оборачивает результат в Optional (T→U даёт Optional<U>). Если f сама возвращает Optional<U> (например, метод findParent(): Optional<...>), то map дал бы неудобный Optional<Optional<U>>. flatMap ожидает функцию T→Optional<U> и «сплющивает» результат в Optional<U>. Так строят цепочки методов, каждый из которых может не вернуть значение: user.flatMap(User::getAddress).flatMap(Address::getCity). Аналогия — flatMap у стримов, но для «0 или 1» элемента.

  3. #optional_null3 / 5
    Что делает ifPresent(consumer) у Optional?
    A)Выполняет действие со значением, только если оно присутствует
    B)Возвращает true или false в зависимости от того, присутствует ли значение внутри Optional
    C)Бросает исключение, если значение отсутствует, вынуждая вызывающего обработать пустой случай
    D)Выполняет переданное действие, подставляя null вместо значения, когда Optional пуст
    показать ответ и разбор
    +A)Выполняет действие со значением, только если оно присутствует

    // разбор: ifPresent(consumer) выполняет переданное действие с содержащимся значением, ЕСЛИ оно есть, и ничего не делает, если Optional пуст — это функциональная замена «if (opt.isPresent()) use(opt.get())». Есть расширенный ifPresentOrElse(consumer, emptyAction) для обеих веток. isPresent() же лишь возвращает boolean. Использование ifPresent/map/orElseGet вместо ручных проверок на наличие делает работу с Optional чище и снижает риск случайного get() на пустом.

  4. #optional_null4 / 5
    Чем отличаются Optional.of(x) и Optional.ofNullable(x)?
    A)Of и ofNullable одинаковы, но of работает быстрее, так как не проверяет аргумент на null
    B)of бросит NPE, если x == null; ofNullable вернёт пустой Optional
    C)Of оборачивает значение, а ofNullable оборачивает null в специальное непустое значение-заглушку
    D)OfNullable бросает исключение при null, а of молча создаёт пустой Optional в этом случае
    показать ответ и разбор
    +B)of бросит NPE, если x == null; ofNullable вернёт пустой Optional

    // разбор: Optional.of(x) требует, чтобы x был НЕ null: при null он сразу бросает NullPointerException — используют, когда null «невозможен» и его появление — это баг, который нужно поймать рано. Optional.ofNullable(x) безопасно оборачивает потенциально null-значение: при null возвращает Optional.empty(), иначе — Optional с x. Ещё есть Optional.empty(). Выбор — по контракту: точно не null → of (быстрый отказ при ошибке); может быть null → ofNullable.

  5. #optional_null5 / 5
    Хорошая ли идея — использовать Optional в качестве поля класса или параметра метода?
    A)Да, поля типа Optional — рекомендуемый стандарт, они делают все классы автоматически потокобезопасными
    B)Да, параметры Optional обязательны везде, где значение может отсутствовать, — это требование Java
    C)Нет: Optional задуман для возвращаемых значений, для полей/параметров он не рекомендуется
    D)Да, Optional-поля сериализуются лучше обычных, поэтому их используют во всех DTO и entity-классах
    показать ответ и разбор
    +C)Нет: Optional задуман для возвращаемых значений, для полей/параметров он не рекомендуется

    // разбор: Optional спроектирован как ТИП ВОЗВРАЩАЕМОГО значения, чтобы явно сигналить «может не быть». Для ПОЛЕЙ его не советуют: он не Serializable, добавляет обёртку/аллокацию на каждый экземпляр и усложняет модель (поле «Optional пустой» vs «поле null» — двусмысленность). Для ПАРАМЕТРОВ тоже: чище дать перегрузку метода или принять обычное значение/null с проверкой. Итог: Optional — на границе (возврат из find/get), а внутри моделей и параметров — обычные типы.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.