Почему не стоит использовать Optional в аргументах в Java 8?
Я прочитал на многих веб-сайтах, что Optional
следует использовать только как тип возвращаемого значения, а не в качестве аргументов методов. Я испытываю трудности с нахождением логической причины для этого. Например, у меня есть логика, которая принимает два параметра Optional
. Поэтому, мне кажется, имеет смысл записать сигнатуру моего метода следующим образом (решение 1):
public int calculateSomething(Optional<String> p1, Optional<BigDecimal> p2) {
// моя логика
}
Однако многие веб-страницы утверждают, что Optional
не следует использовать в качестве аргументов методов. Учитывая это, я мог бы использовать следующую сигнатуру метода и добавить четкий комментарий Javadoc, чтобы указать, что аргументы могут быть равны null
, надеясь, что будущие разработчики прочитают комментарий Javadoc и, следовательно, всегда будут выполнять проверки на null
перед использованием аргументов (решение 2):
public int calculateSomething(String p1, BigDecimal p2) {
// моя логика
}
В качестве альтернативы я мог бы заменить свой метод четырьмя публичными методами, чтобы предоставить более удобный интерфейс и сделать более очевидным, что p1
и p2
являются опциональными (решение 3):
public int calculateSomething() {
calculateSomething(null, null);
}
public int calculateSomething(String p1) {
calculateSomething(p1, null);
}
public int calculateSomething(BigDecimal p2) {
calculateSomething(null, p2);
}
public int calculateSomething(String p1, BigDecimal p2) {
// моя логика
}
Теперь я попробую написать код класса, который вызывает этот кусок логики для каждого подхода. Сначала я получаю два входных параметра из другого объекта, который возвращает Optional
, а затем вызываю calculateSomething
. Если используется решение 1, код вызова будет выглядеть так:
Optional<String> p1 = otherObject.getP1();
Optional<BigInteger> p2 = otherObject.getP2();
int result = myObject.calculateSomething(p1, p2);
Если используется решение 2, код вызова будет выглядеть так:
Optional<String> p1 = otherObject.getP1();
Optional<BigInteger> p2 = otherObject.getP2();
int result = myObject.calculateSomething(p1.orElse(null), p2.orElse(null));
Если применяется решение 3, я мог бы использовать указанный выше код или следующий (но это значительно больше кода):
Optional<String> p1 = otherObject.getP1();
Optional<BigInteger> p2 = otherObject.getP2();
int result;
if (p1.isPresent()) {
if (p2.isPresent()) {
result = myObject.calculateSomething(p1, p2);
} else {
result = myObject.calculateSomething(p1);
}
} else {
if (p2.isPresent()) {
result = myObject.calculateSomething(p2);
} else {
result = myObject.calculateSomething();
}
}
Итак, мой вопрос: Почему считается плохой практикой использовать Optional
в качестве аргументов методов (смотри решение 1)? Это кажется мне самым читаемым решением и больше всего подчеркивает, что параметры могут быть пустыми/нулевыми для будущих поддерживающих разработчиков. (Я понимаю, что создатели Optional
намеревались использовать его только как тип возвращаемого значения, но я не могу найти логические причины, чтобы не использовать его в этом сценарии).
5 ответ(ов)
О, эти стили программирования следует воспринимать с известной долей скептицизма.
- (+) Передача объекта Optional в другой метод, без семантического анализа — это вполне приемлемо; лучше оставить такую проверку самому методу.
- (-) Использование Optional в качестве параметров, приводящее к условной логике внутри методов, абсолютно контрпродуктивно.
- (-) Упаковка аргумента в Optional не является оптимальным решением для компилятора и приводит к ненужной оболочке.
- (-) В сравнении с nullable-параметрами, использование Optional более затратно.
- (-) Существует риск, что кто-то передаст Optional как null в качестве параметра.
В общем: Optional объединяет два состояния, которые необходимо распутать. Поэтому его лучше использовать для возвращаемых результатов, чем для входящих данных, исходя из сложности потока данных.
Существует почти нет убедительных причин не использовать Optional
в качестве параметров. Аргументы против этого чаще всего опираются на авторитет (например, мнение Брайана Гетца, который утверждает, что мы не можем заставить Optional
быть не-null) или на то, что аргументы Optional
могут быть null (по сути, это один и тот же аргумент). Конечно, любая ссылка в Java может быть null, и нам важно улучшить правила, которые будут обеспечиваться компилятором, а не полагаться на память программистов (что является проблематичным и не масштабируется).
Языки функционального программирования поощряют использование параметров типа Optional
. Один из лучших способов применения этого — иметь несколько необязательных параметров и использовать liftM2
для работы с функцией, предполагая, что параметры не пустые, и возвращая значение типа Optional
(см. http://www.functionaljava.org/javadoc/4.4/functionaljava/fj/data/Option.html#liftM2-fj.F-). К сожалению, Java 8 реализовала очень ограниченную библиотеку для поддержки опциональных значений.
Как программисты на Java, мы должны использовать null
лишь для взаимодействия с устаревшими библиотеками.
Если можно позволить себе каламбур, Oracle выпустил оракул:
Не используйте
Optional
ни в каких случаях, кроме возвращаемых значений функций.
Вот мнение, с которым можно не согласиться:
Вы можете полностью устранить
null
из вашей кодовой базы, используяOptional
везде: не только в возвращаемых значениях функций, но также в параметрах функций, в членах классов, в членах массивов и даже в локальных переменных.
Я сделал это в проекте на 100 тысяч строк кода. Это сработало.
Если вы решите пойти по этому пути, вам потребуется быть очень внимательным, поэтому у вас будет много работы. Пример, упомянутый в принятом ответе с Optional.ofNullable()
, никогда не должен возникать, потому что если вы будете тщательно следить за кодом, то не должны иметь null
нигде, и, следовательно, не нужно будет использовать Optional.ofNullable()
.
В том проекте на 100 тысяч строк кода, который я упомянул, я всего пару раз использовал Optional.ofNullable()
, когда получал результаты от внешних методов, над которыми я не имел контроля. Мне никогда не приходилось передавать Optional.ofNullable(nullablePointer)
в качестве параметра функции, потому что у меня нет нулевых указателей: я всегда конвертирую их в optionals в тех немногих местах, где они входят в мою систему.
Теперь, если вы решите следовать этому пути, ваше решение не будет самым производительным, потому что вам придется выделять множество экземпляров Optional
. Однако:
- Это всего лишь недостаток в производительности в режиме выполнения.
- Хотя недостаток и не незначителен, он также не является серьезным.
- Это проблема Java, а не ваша.
Позвольте объяснить последнюю часть.
Java не предлагает явной нулевой восприимчивости для типов ссылок, как это делает C# (с версии 8.0), поэтому в этом отношении она уступает. (Я сказал "в этом отношении"; по другим пунктам Java лучше; но это не по теме сейчас.)
Единственная правильная альтернатива явной нулевой восприимчивости для типов ссылок — это тип Optional
. (И можно утверждать, что он даже немного лучше, потому что с Optional
вы можете указывать optional-of-optional, если это необходимо, тогда как с явной нулевой восприимчивостью вы не можете иметь ReferenceType??
, или по крайней мере это невозможно в C# в текущей реализации.)
Optional
не должен добавлять накладные расходы — он делает это только в Java. Это потому, что Java также не поддерживает истинные типы значений, как C# и Scala. В этом отношении Java значительно уступает этим языкам. (Снова: "в этом отношении"; по другим показателям Java лучше; но это сейчас не по теме.) Если бы Java поддерживала истинные типы значений, то Optional
был бы реализован как одно машинное слово, что означало бы, что накладные расходы на его использование были бы нулевыми.
Таким образом, вопрос, на который в конечном итоге сводится весь этот разговор, заключается в следующем:
Хотите ли вы идеальную ясность и безопасность типов в вашем коде, или предпочитаете максимальную производительность?
Я считаю, что для высокоуровневых языков, к которым Java, безусловно, стремится принадлежать, этот вопрос был решен давно.
Возможно, я спровоцирую множество негативных комментариев и минус-голосов, но... я не могу с этим смириться.
Отказ от ответственности: то, что я напишу ниже, не совсем ответ на оригинальный вопрос, а скорее мои размышления на эту тему. И единственным источником для них являются мои мысли и опыт (в том числе с Java и другими языками).
Первое, давайте рассмотрим, почему вообще кто-то захочет использовать Optional
?
Для меня причина проста: в отличие от других языков, Java не имеет встроенной возможности определять переменные (или типы) как nullable
или non-nullable
. Все переменные типа "объект" могут быть null
, а все примитивные типы — нет. Для упрощения давайте не будем учитывать примитивные типы в дальнейшем обсуждении, и я просто утвержу, что все переменные могут быть null
.
Почему же нужно обозначать переменные как nullable
или non-nullable
? Для меня причина в том, что явное всегда лучше подразумеваемого. Кроме того, явное обозначение (например, аннотация или тип) может помочь статическому анализатору (или компилятору) выявить некоторые проблемы, связанные с нулевыми указателями.
Многие люди утверждают в комментариях выше, что функции не должны иметь nullable
аргументы. Вместо этого следует использовать перегрузку. Но такое утверждение верно только в учебниках. В реальной жизни бывают разные ситуации. Рассмотрим класс, который представляет настройки некоторой системы или персональные данные пользователя, или, в принципе, любую составную структуру данных, содержащую множество полей — многие из которых имеют повторяющиеся типы, и некоторые поля обязательные, а другие необязательные. В таких случаях наследование/перегрузка конструкторов на самом деле не помогают.
Случай на размышление: предположим, нам нужно собрать данные о людях. Но некоторые не хотят предоставлять все данные. И, конечно, это POD, то есть тип со семантикой значения, поэтому я хочу, чтобы он был более или менее неизменяемым (без сеттеров).
class PersonalData {
private final String name; // обязательное
private final int age; // обязательное
private final Address homeAddress; // необязательное
private final PhoneNumber phoneNumber; // необязательное. Отдельный класс для обработки ограничений
private final BigDecimal income; // необязательное.
// ... дальнейшие поля
// Сколько перегрузок конструктора (или фабрики) нам нужно, чтобы обработать все случаи
// без использования nullable аргументов? Если я не ошибаюсь, 8. А что если у нас больше необязательных
// полей?
// ...
}
Таким образом, ИМХО, вышеуказанная дискуссия показывает, что, хотя в большинстве случаев мы можем обойтись без nullable
аргументов, иногда это действительно нецелесообразно.
Теперь мы подходим к проблеме: если некоторые аргументы могут быть null
, а некоторые нет, как нам это узнать?
Подход 1: Все аргументы являются nullable
(согласно стандарту Java, кроме примитивных типов). Так что проверяем все из них.
Результат: код разрастается с не нужными проверками, большинство из которых ненужны, так как, как мы уже обсуждали, почти всё время мы можем обойтись nullable
переменными, и только в редких случаях nullable
действительно необходимы.
Подход 2: Использовать документацию и/или комментарии для описания того, какие аргументы/поля являются nullable
, а какие нет.
Результат: Это на самом деле не работает. Люди ленивы писать и читать документацию. Кроме того, в последние годы существует тенденция избегать написания документации в пользу создания самодокументирующегося кода. Кроме того, все доводы о том, что можно изменить код и забыть обновить документацию, всё еще актуальны.
Подход 3: @Nullable
, @NonNull
и т. д. Я лично нахожу их полезными. Но есть определенные недостатки (например, они уважаются только внешними инструментами, но не компилятором), худший из которых заключается в том, что они не стандартизированы, что означает, что 1. мне нужно будет добавить внешнюю зависимость в мой проект, чтобы воспользоваться ими, и 2. Их обработка различными системами не унифицирована. Насколько я знаю, они были исключены из официального стандарта Java (и я не в курсе, есть ли планы попробовать снова).
Подход 4: Optional<>
. Недостатки уже были упомянуты в других комментариях, худший из которых (по моему мнению) — это потеря производительности. Также это добавляет немного шаблонного кода, хотя я сам считаю, что использование Optional.empty()
и Optional.of()
не так уж и плохо. Преимущества очевидны:
- Это часть стандарта Java.
- Это ясно дает понять читателю кода (или пользователю API), что эти аргументы могут быть
null
. Более того, это заставляет как пользователя API, так и разработчика метода признать этот факт, явно завернув/развернув значения (что не является случаем, когда используются аннотации вроде@Nullable
и т. д.).
Таким образом, с моей точки зрения, нет черно-белых решений по поводу любой методологии, включая эту. Я лично пришёл к следующим рекомендациям и соглашениям (которые все еще не являются строгими правилами):
- В моем собственном коде все переменные должны быть not-null (но возможно
Optional<>
). - Если у меня есть метод с одним или двумя необязательными аргументами, я пытаюсь перепроектировать его с использованием перегрузок, наследования и т. д.
- Если я не могу найти решение за разумное время, я начинаю думать, критична ли производительность (т. е. если нужно обработать миллионы объектов). Обычно это не так.
- Если нет, я использую
Optional
в качестве типов аргументов и/или полей.
Существуют все еще серые зоны, в которых эти соглашения не работают:
- Нам нужна высокая производительность (например, обработка огромных объемов данных, так что общее время выполнения очень велико, или ситуации, когда критична пропускная способность). В таких случаях потеря производительности, вызванная использованием
Optional
, может быть действительно нежелательной. - Мы находимся на границе кода, который мы пишем сами, например: мы читаем из БД, REST-эндапоинта, разбираем файл и т. д.
- Или мы просто используем некоторые внешние библиотеки, которые не следуют нашим соглашениям, так что снова стоит быть осторожными...
Кстати, последние два случая также могут стать причиной необходимости в опциональных полях/аргументах. То есть, когда структура данных не разработана нами, а наложена внешними интерфейсами, схемами БД и т. д.
В конце концов, я думаю, что нужно задуматься о решаемой проблеме и попытаться найти подходящие инструменты. Если Optional<>
уместен, то я не вижу причин его не использовать.
Правка: Подход 5: Я использовал его недавно, когда не мог использовать Optional
. Идея заключается в том, чтобы использовать соглашение о наименовании для аргументов методов и переменных класса. Я использовал префикс "maybe", поэтому, если, например, аргумент "url" может быть null
, он становится maybeUrl
. Преимущество в том, что это немного улучшает понимание намерений (и не имеет недостатков других подходов, таких как внешние зависимости или потеря производительности). Но есть и недостатки: например, нет инструментов, чтобы поддерживать это соглашение (ваш IDE не покажет вам никаких предупреждений, если вы обращаетесь к "maybe"-переменной, не проверив её сначала). Ещё одна проблема заключается в том, что это помогает только в том случае, если все участники проекта применяют его последовательно.
Этот совет является разновидностью правила «будьте как можно менее конкретными в отношении входных данных и как можно более конкретными в отношении выходных данных».
Обычно, если у вас есть метод, который принимает обычное ненулевое значение, вы можете применить его к Optional
, что делает обычную версию строго менее конкретной в отношении входных данных. Тем не менее, есть несколько возможных причин, по которым вы всё же захотите требовать аргумент Optional
:
- вам нужно, чтобы ваша функция использовалась вместе с другим API, который возвращает
Optional
- ваша функция должна возвращать что-то иное, чем пустой
Optional
, если переданное значение пустое вы считаете, чтоOptional
так потрясающ, что кто бы ни использовал ваше API, должен быть обязан изучить его 😉
Сортировка ArrayList пользовательских объектов по свойству
Как установить Java 8 на Mac
Java 8: Преобразование List<V> в Map<K, V>
Как преобразовать список списков в список в Java 8?
Найти первый элемент по предикату