Статьи

Дополнительные опции Java для более выразительного кода

Любой из нас, кто программировал на языке, который допускает нулевые ссылки, столкнулся с тем, что происходит, когда вы пытаетесь разыменовать его. Вне зависимости от того, приводит ли это к segfault или NullPointerException, это всегда ошибка. Тони Хоар назвал это своей ошибкой в ​​миллиард долларов . Проблема обычно возникает, когда функция возвращает пустую ссылку на клиент, который был неожиданным для разработчика клиента. Скажем, в коде так:

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

Проницательный программист сразу же спросит, что происходит, когда не найдено ни одного пользователя, соответствующего «Алисе», но ничто в сигнатуре метода find() не говорит вам, чего ожидать. Типичным Java-решением для этого в прошлом было бы заставить метод генерировать проверенное исключение, возможно, исключение UserNotFoundException . Это, безусловно, сообщит клиентскому программисту, что такая возможность может произойти, но это ничего не сделает для повышения выразительности их кода. Ловля исключений приводит к коду, который мешает пониманию. В любом случае, проверенные исключения перестали нравиться, и люди больше не пишут код, который их генерирует.

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

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

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

Лямбды делают вещи лучше

Это решение уже намного лучше, но есть еще кое-что в Optional : если вы продолжаете обрабатывать дополнительные функции таким образом, вы упускаете некоторые возможности, чтобы сделать ваш код более выразительным.

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

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

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

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

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

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

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

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

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

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

1
2
3
4
if (optionalValue.isPresent()) {
    return optionalValue.get();
}
return defaultValue;

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

1
return optionalValue.orElse(defaultValue);

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

1
2
3
4
value = methodThatMayReturnNull();
if (value == null) {
    value = defaultValue;
}

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

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

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

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

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

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

Вы не должны этого делать, потому что createUser() всегда будет вызываться независимо от того, присутствует опциональный или нет! Это почти наверняка не то, что вам нужно: в лучшем случае вы сделаете ненужный вызов метода, и, если метод имеет побочные эффекты, он может привести к ошибке. Вместо этого вы должны вызвать Optional.orElseGet() и предоставить ему Supplier который предоставляет значение по умолчанию:

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

Теперь createUser() будет вызываться только в том случае, если необязательный параметр отсутствует, что является createUser() поведением. Еще раз мы можем заменить лямбду ссылкой на метод:

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

Бросать исключения

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

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

Сопоставление дополнительных значений

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

01
02
03
04
05
06
07
08
09
10
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.map() . Это очень похоже на метод map в Stream API. Он принимает Function и вызывает ее, когда присутствует дополнительная Function . Он передает упакованное значение в качестве аргумента функции и переносит возвращаемое значение в другой необязательный параметр. Таким образом, в этом случае он сопоставляет Optional<Amount> с Optional<String> . Это позволяет нам переписать код следующим образом:

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

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

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

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

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

Опубликовано на Java Code Geeks с разрешения Ричарда Уайлда, партнера нашей программы JCG . См. Оригинальную статью здесь: дополнительные возможности Java для более выразительного кода

Мнения, высказанные участниками Java Code Geeks, являются их собственными.