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