Optional и работа с null
Поставил счётчик вызовов на дорогую функцию и позвал её как значение по умолчанию для НЕПУСТОГО 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, остальные разбираются в тренажёре.
- Почему Optional.get() без проверки считается плохой практикой?A)Get() возвращает null для пустого Optional, из-за чего снова возникает та же проблема с NPEB)Get() изменяет состояние Optional, делая его пустым после первого вызова, поэтому повторно не работаетC)Бросит NoSuchElementException, если Optional пуст — теряется смысл OptionalD)Get() медленно работает, так как каждый раз заново разворачивает обёртку через тяжёлую рефлексию
показать ответ и разбор
+C)Бросит NoSuchElementException, если Optional пуст — теряется смысл Optional// разбор: get() возвращает значение, но на ПУСТОМ Optional бросает NoSuchElementException. Вызывать его без предварительной проверки isPresent() — по сути тот же необработанный «null», ради ухода от которого Optional и придуман. Идиоматично вместо get() использовать безопасные методы: map/flatMap/filter (преобразования), orElse/orElseGet (значение по умолчанию), orElseThrow (осмысленное исключение), ifPresent/ifPresentOrElse (действие). isPresent()+get() — это «замаскированная null-проверка», обычно её переписывают функционально.
- Чем 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» элемента.
- Что делает ifPresent(consumer) у Optional?A)Выполняет действие со значением, только если оно присутствуетB)Возвращает true или false в зависимости от того, присутствует ли значение внутри OptionalC)Бросает исключение, если значение отсутствует, вынуждая вызывающего обработать пустой случайD)Выполняет переданное действие, подставляя null вместо значения, когда Optional пуст
показать ответ и разбор
+A)Выполняет действие со значением, только если оно присутствует// разбор: ifPresent(consumer) выполняет переданное действие с содержащимся значением, ЕСЛИ оно есть, и ничего не делает, если Optional пуст — это функциональная замена «if (opt.isPresent()) use(opt.get())». Есть расширенный ifPresentOrElse(consumer, emptyAction) для обеих веток. isPresent() же лишь возвращает boolean. Использование ifPresent/map/orElseGet вместо ручных проверок на наличие делает работу с Optional чище и снижает риск случайного get() на пустом.
- Чем отличаются Optional.of(x) и Optional.ofNullable(x)?A)Of и ofNullable одинаковы, но of работает быстрее, так как не проверяет аргумент на nullB)of бросит NPE, если x == null; ofNullable вернёт пустой OptionalC)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.
- Хорошая ли идея — использовать Optional в качестве поля класса или параметра метода?A)Да, поля типа Optional — рекомендуемый стандарт, они делают все классы автоматически потокобезопаснымиB)Да, параметры Optional обязательны везде, где значение может отсутствовать, — это требование JavaC)Нет: Optional задуман для возвращаемых значений, для полей/параметров он не рекомендуетсяD)Да, Optional-поля сериализуются лучше обычных, поэтому их используют во всех DTO и entity-классах
показать ответ и разбор
+C)Нет: Optional задуман для возвращаемых значений, для полей/параметров он не рекомендуется// разбор: Optional спроектирован как ТИП ВОЗВРАЩАЕМОГО значения, чтобы явно сигналить «может не быть». Для ПОЛЕЙ его не советуют: он не Serializable, добавляет обёртку/аллокацию на каждый экземпляр и усложняет модель (поле «Optional пустой» vs «поле null» — двусмысленность). Для ПАРАМЕТРОВ тоже: чище дать перегрузку метода или принять обычное значение/null с проверкой. Итог: Optional — на границе (возврат из find/get), а внутри моделей и параметров — обычные типы.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.