Выразительный код с Java Optionals

Каждый из нас, кто программировал на языках, которые позволяют ссылаться на null, знает, что произойдет, когда вы попытаетесь получить значение подобной переменной. Независимо от того, будет ли это segfault или появление NullPointerException, это всегда является ошибкой. Tony Hoare описал это поведение как ошибку на один миллиард долларов (https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare). Обычно проблема появляется тогда, когда функция возвращает ссылку на null клиенту, в то время как это совсем не ожидалось разработчиком клиента. Скажем, в коде подобном такому:

User user = userRepository.find("Alice");

Сообразительный программист сразу подумает о том, что должно произойти, когда мы не найдем пользователя с именем «Alice», однако в самой сигнатуре метода find() так ничего и не будет говорить, что подобное поведение возможно. Типичное решение в мире Java в прошлом заключалось бы в том, что метод выбрасывал бы проверяемое исключение, например UserNotFoundException, которое определённо бы говорило разработчику клиента, что подобное поведение возможно, однако никаким образом не улучшало бы выразительность кода. Отлов исключений в коде мешает его пониманию. Также следует заметить, что работа с проверяемыми исключениями больше не в почёте, и люди больше не стремятся писать код, который бы выбрасывал их.

Много разработчиков вместо этого прибегают к выбрасыванию непроверяемых исключений или к возврату ссылки на null. Каждый их этих подходов плох по той же причине: ни один из них не информирует разработчика о подобной ситуации и в обоих случаях возникнет ошибка времени выполнения, в случае, если ситуация не будет обработана корректно. В Java 8 появился тип Optional для решения подобной проблемы. Всякий раз, когда вы пишите метод, который может как вернуть значение, так и вернуть null вы должны использовать Optional в качестве возвращаемого значения метода. Так в примере выше, find() мог бы возвращать значение типа Optional<User>. Код клиента должен теперь выполнить несколько дополнительных шагов, чтобы проверить наличие или отсутствие значения и его получения.

Optional<User> userOpt = userRepository.find("Alice");
if (userOpt.isPresent()) {
    User user = userOpt.get();  
}

Более того, если в коде вызов get() происходит без проверки, то IDE вероятно сможет предупредить об этом.

C лямбдами лучше

Решение уже значительно лучше, но Optional может ещё лучше, и если вы придерживаетесь обработки ошибок подобным образом, то вы теряете возможность сделать ваш код более выразительным.

Отрывок кода выше был адаптирован из моей собственной имплементации упражнения «социальной сети», который используется в Codurance для тестирования соискателей на работу. В действительности, мой код более похож на:

Optional<User> userOpt = userRepository.find(subject);
if (userOpt.isPresent()) {
    User user = userOpt.get();
    printAllMessagesPostedToUser(user);
}

У Optional есть метод ifPresent() который позволяет нам предоставить объект класса Consumer, в случае, если объект Optional представлен. Аргументом потребителя (Consumer) будет являться найденный объект. Это позволяет нам переписать код следующим образом:

userRepository.find(subject).ifPresent(user -> printAllMessagesPostedToUser(user));

Мы можем даже пойти на ещё один шаг вперёд и использовать ссылку на метод вместо лямбда-выражения.

userRepository.find(subject).ifPresent(this::printAllMessagesPostedToUser);

Я думаю, что это сообщает намерения программиста (в данном случае меня) значительно более ясно чем выражение if.

Раздражает отсутствие подобного метода ifNotPresent() для случая, когда значение Optional отсутствует, и даже если бы был, то ifPresent(), который  возвращает void не может быть связан последующей цепочкой вызовов методов. Java 9 идет в направлении исправления этого с ifPresentOrElse(Consumer<T>, Runnable), но это всё ещё далеко от идеала.

Подстановка значений по умолчанию

Что мы можем сделать, в случае, если значение Optional не представлено? Не уставая жаловаться на ifPresent() которая является единственно подходящей лишь для команд с побочным эффектом. Если бы мы имплементировали получение значения с подстановкой значения по умолчанию, то в случае Optional это может выглядеть так:

if (optionalValue.isPresent()) {
    return optionalValue.get();
}
return defaultValue;

Подобное поведение может быть легко достигнуто при помощи Optional.orElse():

return optionalValue.orElse(defaultValue);

Этот подход также представляет удобный путь для де-нуллификации значений, когда вы вызываете метод, который может возвращать null и этот метод не находится под вашим контролем. Мы все писали код подобный следующему, ранее:

value = methodThatMayReturnNull();
if (value == null) {
    value = defaultValue;
}

Вы можете использовать Optional.ofNullable() для рефакторинга вашего кода, т.к. метод возвращает Optional.empty() в случае, если значение null:

value = Optional.ofNullable(methodThatMayReturnNull()).orElse(defaultValue);

Я думаю, читать это несколько легче, чем ObjectUtils.defaultIfNull который выполняет тоже самое. Однако есть одно предостережение. Вы не должны использовать Optional.orElse() для вызова метода, который имеет побочные эффекты. Для примера, где-то в моём упражнении с социальной сетью у меня есть код, который выполняет поиск пользователя и возвращает его, если найден, в противном же случае создаётся новый пользователь:

Optional<User> userOpt = userRepository.find(recipient);
if (userOpt.isPresent()) {
    return userOpt.get();
}
return createUser();

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

return userRepository.find(recipient).orElse(createUser());

Но, не делайте так, поскольку createUser() будет вызываться всякий раз, независимо от того, представлено ли значение в Optional или нет. Скорее всего, это определённо не то, чего вы хотите. В лучшем случае, вы сделаете лишний вызов метода, в случае же, если у метода есть побочные эффекты, то это приведёт к появлению ошибки. Вместо этого, вы должны выполнить вызов Optional.orElseGet() и передать ему объект Supplier’a который доставит значение по умолчанию.


return userRepository.find(recipient).orElseGet(() -> createUser());

Сейчас createUser() будет вызван лишь тогда, когда значение Optional не представлено, т.е. то поведение, которое я и хочу. И вновь, мы может заменить лямбду с ссылкой на метод:

return userRepository.find(recipient).orElseGet(this::createUser);

Выброс исключений

Может случиться так, что вы захотите выбросить исключение, в случае, если необязательное значение не представлено. Вы можете сделать это выполнив вызов Optional.orElseThrow() и передать в него объект Supplier который создаст новое исключение:

return userRepository.find(recipient)
        .orElseThrow(() -> new RuntimeException("User " + recipient + " not found"));

Преобразование (mapping) необязательных значение

У Optionl есть несколько методов, которые позволяют вам выполнить операции преобразования подобные операциям у Steram. Для примера в другом упражнении, у меня был код подобный следующему:

Optional<Amount> creditAmountOpt = transaction.getCreditAmount();
Optional<Amount> debitAmountOpt = transaction.getDebitAmount();

String formattedDepositAmount = creditAmountOpt.isPresent() ?
        formatAmount(creditAmountOpt.get()) : " ";
String formattedWithdrawalAmount = debitAmountOpt.isPresent() ?
        formatAmount(debitAmountOpt.get()) : " ";

return String.format(" %s| %s|", formattedDepositAmount, formattedWithdrawalAmount);

Данный код был в контексте класса, который печатал строку банковской выписки. Мой класс Transaction конечно же знал о вводе или выводе денег, но я не хотел, чтобы этого знал принтер, печатающий строки банковской выписки. Поэтому интерфейс Transaction возвращал объект типа Optional с суммой дебита или кредита. Принтер должен форматировать значение, если оно представлено, а если нет, то подставлять пустую строку по умолчанию.

Для устранения условных операторов, мы можем использовать метод Optional.map(), который очень похож на подобный map метод в Stream API. Он принимает Function в качестве значения и вызывает его, когда необязательное значение представлено. В Function передаётся необязательное значение, которое было представлено Optional и возвращаемое значение от Function оборачивается в Optional также. Например, в данном случае выполнится преобразование из Optional<Amount> в Optional<String>. Это позволяет нам переписать код следующим образом:

return String.format(" %s| %s|",
        transaction.getDepositAmount().map(this::formatAmount).orElse(" "),
        transaction.getWithdrawalAmount().map(this::formatAmount).orElse(" "));

Вы можете спросить, что произойдет, в случае, если ваша функция преобразования возвратит другой Optional, например: Function<T, Optional<U>> – в этом случае результатом выполнения функции map() будет тип Optional<Optional<U>>, который вероятно вы не хотите. И вновь, подобно методам Stream API существует функция flatMap(), которая возвратит значение Optional<U>.

Сходство со Stream API продолжается методом Optiona.filter() который вычисляет представленный предикат, если необязательное значение представлено и когда предикат вычисляется в ложь, тогда фильтр вернет пустой Optional. Необязательные значения лучше использовать для простого, но достаточно длинного кода, для получения простого, но более краткого.

Но будьте осторожны

В завершение, следует сказать, что любым инструментом можно злоупотреблять и Optional здесь не исключение. Optional предназначен лишь для представления возвращаемых значений. IntelliJ выдаст вам предупреждение, если вы попытаетесь объявить переменную объекта типа Optional, т.к. это выглядит как объявление временного поля, которое рассматривается как «гниение кода». Также объекты Optional не должны использоваться в качестве параметров метода: по существу, это замаскированное булево значение, которое также можно отнести к «гниению кода». Если же вы хотите этого, то лучше разделить ваш метод на два, первый принимает параметр и второй, который работает без параметров, и поместить условный оператор в коде клиента.

Эта статья впервые была опубликована на сайте Codurance (https://codurance.com/publications/) автором Richard Wild
Перевод: A.Saushkin