Это пятая и последняя статья серии, посвящённая использованию класса Optional при обработке объектов с динамической структурой. В первой статье было рассказано о способах избежания NullPointerException в ситуациях, когда вы не можете или не хотите использовать Optional. Вторая статья посвящена описанию методов класса Optional в том виде, как он появился в Java 8. Третья — методам, добавленным в класс в Java 9. В четвертой статье я представил класс, который расширяет возможности класса Optional на случай, когда при неудаче при обработке данных в методе нам необходимо обрабатывать информацию об этой неудаче.
В этой статье мы рассмотрим вопрос, стоит ли Optional использовать во всех тех местах, где могут появиться нулевые значения. Я приведу также мнение Brian Goetz, архитектора языка Java v Oracle об этом классе, и конечно, исполню данное в прошлой статье обещание — поощрю каждого читателя, прочитавшего все статьи серии, ценным подарком.
А ведь всё могло быть не так
Перед тем как мы перейдём к рассмотрению полезных с практической точки зрения вопросов использования Optional в разных ролях внутри наших классов, я хотел бы сделать одно маленькое, но с моей точки зрения необходимое, историческое отступление.
Когда проблема расширения Java элементами ФП (Функционального Программирования) назрела, спектр возможных решений представлялся очень широким. В максимальном варианте можно было бы попробовать расширить Java реализацией важнейших парадигм ФП, опробованные к тому времени в таких языках программирования как LISP, ML, Haskell, OCaml, F#, Erlang, Clojure и Scala.
Критики максималистского подхода предупреждали, что реализации некоторых парадигм ФП могут препятствовать ранее принятые при реализации языка архитектурные решения. Кроме того, они опасались, что введение некоторых слишком сложных для освоения парадигм приведёт к их слабому использованию. А это, в свою очередь, грозило тем, что и весь комплекс нововведений будет проигнорирован большинством пользователей Java.
Так или иначе, в конце концов победил подход, провозглашённый как «прагматический». В Java 8 появились только основные элементы ФП в виде потоков, функций, лямбд и Optional.
В рамках победившего подхода Optional в официальной документации не претендовал на роль одной из реализации парадигмы монады из ФП. (Не претендовать на роль, не значит не играть её, смотрите детали здесь. Вместо этого класс Optional позиционировался в официальной «разъясняющей» статье как контейнер для потенциально нулевых объектов.
А где его ещё применить?
А раз так, то некоторые пользователи задались естественным вопросом: Получается, везде где в классе может появиться null, надо теперь использовать Optional?
Упомянутая «разъясняющая» статья Oracle однозначно говорит — Нет!
А именно (в моём переводе):
«Назначение класса Optional не состоит в том, чтобы заменять собой каждую ссылку на потенциально нулевой объект. Целью является возможность разработки более понятных API так, чтобы, просто по сигнатуре метода определить, что возвращается Optional. Это заставляет вас активно анализировать Optional, чтобы разобраться с его значением"
Пусть так. Но где, на каких «местах» или «ролях» в классе, можно было бы использовать Optional?
Optional как значение поля?
Давайте разберёмся конкретно и начнем с переменной (поля) класса. Посмотрим упомянутую статью. Там приводится вот такой код:
public class Computer {
private Optional<Soundcard> soundcard;
public Optional<Soundcard> getSoundcard() { ... }
...
}
Ага! Значит использовать Optional в качестве переменной класса можно?
Можно то можно, но вот что надо помнить:
- Класс Optional не реализует интерфей Serializeble. Это означает, что переменная не будет правильно сохранена или считана с носителя с помощью используемых это свойство фреймворков.
- Optional это так называемый value-based объект. Это подразумевает в частности особые правила сравнения инстанций объектов между собой, о чём можно в деталях почитать здесь. Это означает в свою очередь, что некоторые фреймворки, использующие внутри механизмы Reflection, могут неправильно обработать такие поля.
Таким образом, прежде чем использовать Optional как хранитель значения поля в вашем классе, убедитесь, что потенциальным пользователям вашего класса не придётся его сериализировать или обрабатывать с помощью не готовых к этому фраймворков.
Поэтому некоторые авторы советуют не связываться с этими потенциальными головными болями и хранить в полях вместо Optional сами объекты, помня при программировании класса, что они могут быть нулевыми. И только «выпуская» объект наружу в качестве возвращаемого значения помещать его в «футляр» Optional.
Использование Optional в setters
Ну а как с методами задания потенциально нулевых полей класса? Когда стоит предлагать в вашем API метод с сигнатурой типа setSomeObject(Optional<SomeObject> opt) а когда нет?
Очевидный случай — когда вы строите фасад вокруг существующего класса у которого есть метод setSomeObject(SomeObject obj) и obj может быть нулевым. В этом случае вы по крайней мере в покрывающем «фасадном» методе явно уведомляется пользователя о возможности использования нулевого объекта.
В оставшихся случаях почти всегда лучше создавать нормальный setter-метод с ограничением (и возможно проверкой), что передаваемый метод никогда не будет равен null. На этапе разработки такое ограничение можно проверять с помощью assert. Проверять ли его в продакштн и если да, то как — это отдельный интересный вопрос, выходящий за рамки нашего обсуждения.
Таким образом, вслед за многими авторами я призываю вас не предполагать и не допускать передачи null в setter-ах.
Как же так, воскликнет внимательный читатель. Во второй статье серии автор сам предлагал использовать вот такой интерфейс:
public interface IBoilerInput2 {
void setAvailability(@Nullable CupOfWater water, boolean powerAvailable);
}
с параметром water, который может принимать значение null, а теперь говорит что это плохо? Да. Именно так. Чтобы сконцентрировать ваше внимание на основном аспекте я в учебном примере позволил себе это. Но при разработке реальных API, которые могут использовать другие разработчики, такого надо избегать. Но как?
Давайте рассмотрим ситуацию внимательнее. Передача null в качестве значения параметра означает как правило изменение конфигурации объекта, отключение части его возможностей либо переключение его поведения в новый режим. Подберите соответствующий термин, описывающий такое переключение.
Упомянутый выше интерфейс преобразился бы тогда примерно так:
public interface IBoilerInputX {
void setWater(@Nonnull CupOfWater water);
void setWaterDisabled();
void setPowerAvailable( boolean powerAvailable);
}
Новый метод setWaterDisabled явно указывает на предстоящее изменение поведения прибора в отличие от предыдущего варианта в IBoilerInput2, где это достигалось вызовом параметра с нулевым значением.
Использование Optional в группе параметров
В некоторых случаях, однако, некоторую группу параметров нельзя по концептуальным или техническим соображениям разбить на отдельные пары из setter и переключателей и вам просто необходимо передать всю группу параметров в одном вызове. Очень часто такое случается с конструкторами класса. При этом некоторые параметры группы могут отсутствовать, быть неготовыми и т.д. — т.е. иметь значение null. В этом случае использование футляра Optional много лучше, чем передача потенциально нулевых параметров напрямую. Вероятность того, что какой-то по ошибке занулившийся в предыдущих вычислениях параметр попадёт внутрь вашего класса и вызовет потом NPE или неверное поведение, в этом случае существенно уменьшается.
Итоговые рекомендации
Подведем предварительные итоги в виде прагматических рекомендаций:
- Используйте Optional в setter или в методах с одним параметром (только) если строите фасад для метода явно предполагающего передачу параметра, который может принимать значение null.
- В большинстве случаев, если параметр может принимать нулевое значение, передавайте его через setter с ограничением на не-нулевое значение и сопутствующим методом переключения режима работы объекта. Внутри переключателя зануляйте значение соответствующего поля.
- Если ваш метод должен иметь много параметров, часть из которых могут принимать нулевые значения, передавайте такие параметры внутри футляра Optional. Это заставит клиента, вызывающего ваш метод явно разобраться с ситуацией перед «вкладыванием» объекта в футляр.
«Неполноценные» возвращаемые значения
В первой и второй статьях этой серии я долго убеждал читателей в том, что потенциально нулевые возвращаемые значения надо обязательно перед выпуском наружу упаковывать в футляр — Optional. Это всегда хорошо? К сожалению, где свет, там и тень.
Сложные объекты, особенно в энтерпрайзных системах представляют собой нередко агрегации или иерархии более простых объектов, которые доступны снаружи с помощью вызова getter-методов. То, что объект возвращает «пустой» объект (неважно, упакованный в футляр или нет) часто не означает, что внутри агрегата или иерархии такой объект отсутствует. В реальных системах это часто означает, что объект в каком-то смысле «неполноценен» либо конфигурация содержащего его агрегат-объекта не позволяет его использовать во внешнем мире. С этой точки зрения возращение Optional.empty() оправдано. Однако, если мы захотим с помощью внешнего framework (без использования Reflection или манипулирования байкодом) сохранить объект на внешнем носителе либо передать по сети, «неполноценность» объекта либо неподходящая конфигурация содержащего его агрегата не должны быть нам помехой. Получается, getter возвращающий футляр Optional нам в этом случае использовать нельзя. Как же так? За что боролись?
На самом деле всё стало только лучше. Да, нам придётся реализовывать специальные методы для персистирования частей большого агрегата. Но при этом их лучше собрать в один отдельный интерфейс, а методы реализации бизнес-логики в другой. Тем самым мы сможем разделить бизнес-аспект и технический аспект использования одного и того же объекта.
Optional вместо «магических» чисел
Аналогом null из мира объектов в мире чисел служат «магические» числа. Т.е. такие значения параметра, с которыми связана радикально иная семантика, чем с «нормальными» числами.
Даже стандартные библиотеки Java грешат этим. Документация к конструктору класса java.net.Socket:
public Socket(String host,
int port,
InetAddress localAddr,
int localPort)
throws IOException
говорит нам про параметр localPort, что если он задан ненулевым, система будет его использовать. А если значение параметра 0 — она поищет вам свободный порт.
Налицо мы имеем радикально разное поведение метода при ненулевом и нулевом значении параметра.
Чтобы в будущем можно было избегать подобных ситуаций, в Java 8 были введены классы OptionalInt и OptionalDouble. Используйте эти классы, если ваш метод должен возвращать некое число в случае успеха, но не-успех также возможен.
А вместо заключения — думайте сами!
Я посвятил пять статей описанию вариантов использования методов класса Optional. Думаю в Java не так много классов, которые вызывают столько вопросов и споров.
А кстати, уважаемые читатели, как вы думаете, сколько строк кода (не считая комментариев и вместе с ними) содержит этот класс?
Некоторые из статей этой серии породили весьма острые и интересные дискуссии. Но это даже не капля в море, а молекула капли по сравнению с дискуссиями и вопросами, появившемся в англоязычном секторе Интерната после появления Optional в Java 8. Одной из самых жарких и интересных дискуссий про Optional явилась вот эта: Should Java 8 getters return optional type?
В дискуссии принял участие и Brian Goetz, занимающий в Oracle позицию «Java Language Architect». Увидев, как Java-программисты используют его детище, вот что он написал в форуме (перевод мой):
Конечно, люди будут делать то, что хотят. Но у нас было четкое намерение при добавлении этой функциональности, и мы не ставили цель создать механизм наподобие Maybe или Some в других языках. Хотя многим этого возможно и хотелось. Наша цель заключалось в том, чтобы дать компактный механизм для определения типов возвращаемых значений с четким выделением ситуации «никакого результата», как альтернативу использованию нулевого значения, что в подавляющем большинстве случаев может вызвать ошибки.
Например, вы, вероятно, никогда не должны использовать его для чего-то, что возвращает массив результатов или список результатов; вместо этого возвратите пустой массив или список. Вы почти никогда не будете использовать его как поле чего-либо или параметр метода.
Я думаю, что его регулярное использование в качестве возвращаемого значения для геттеров определенно будет чрезмерным.
В Optional нет ничего плохого, из за чего его следует избегать. Это просто не то, что многие хотели бы видеть на его месте. И поэтому мы были весьма обеспокоены риском чрезмерного использования.
Обращение: НИКОГДА не вызывайте Optional.get, если вы не можете доказать, что он (в этом контексте) всегда не равен нулю. Вместо этого используйте один из безопасных методов, например orElse или ifPresent. В ретроспективе можно сказать, мы должны были бы вместо get предоставить что-то вроде getOrElseThrowNoSuchElementException, что сделало бы его использование более ясным. Но это — Lesson learned.
Другими словами, создатели языка не всегда расширяют его так как хотелось бы некоторым (продвинутым) пользователям. Пользователи не всегда используют новые возможности языка как задумывали их создатели. И создатели иногда жалеют о допущенных ошибках, которые уже не поправить.
Так что, дорогие читатели, думайте сами, решайте сами, как применять Optional в ваших проектах. А мне остаётся надеяться, что эта серия статей и обещанный подарок — постер про Optional, помогут вам в этом.
Обратите внимание на названия групп методов класса слева. Они помогут вам быстрее найти нужный метод.
Девятка в кружке говорит о том, что метод доступен начиная с Java 9.
Последняя колонка освещает семантику параметров.
Семантика типов входных параметров (слева) и результатов (справа) позволит вам быстро вспомнить, что делает метод в случае если нулевого и не-нулевого значения параметра.
Например, для метода Optional.of():
(x, Ø)=>(O(x),¥)
не-нулевой параметр x отобразится в Optional от него, что отображено как O(x),
а при попытке использовать нулевой входной параметр (обозначенный как Ø), вы получите Exception, обозначенное символом Йены: ¥.
Автор: visirok