В нынешнее время для большинства разработчиков стали очевидными минусы использования null как возвращаемых типов или передачи их как аргумента.
Младшие разработчики даже если не понимают, то обычно следуют "чистому коду" (прочитав книжку Роберта Мартина). Поэтому код с возможностью возникновения NPE стал встречаться реже, хотя, конечно, они есть.
Не хочу сказать, что любое использование null - это плохо, скорее тут можно сказать "семь раз отмерь, один раз отрежь".
Тем не менее, я долгое время не жаловался насчет NPE, даже когда разработчики с языков у которых строгий контроль на эту тему хвастались своим null-safety. Но из-за одного бага я понял, что просто использование null это не так страшно, даже если вы возвращаете или передаете его. Разумеется, это очень плохо, но есть вещи хуже - null в спецификациях.
Было бы не особо интересно, если рассказ был об одной компании, которая сделала плохую спецификацию. Давайте вместо этого поговорим о более известной спецификации из Java EE - Java Servlet Specification, а конкретно возьмем класс HttpServletRequest и заглянем в метод getCookies()
getCookies
Cookie[] getCookies()
Returns an array containing all of the Cookie
objects the client sent with this request. This method returns null
if no cookies were sent.
Returns:
an array of all the
Cookies
included with this request, ornull
if the request has no cookies
Больше всего следует обратить внимание на это:
This method returns
null
if no cookies were sent.
То есть, если здесь нет куков, то нужно вернуть null. Посмотрим на это со стороны разработчика:
Нам нужно вернуть массив из куки, а если их нет, то что-то, что означало бы, что куков в запросе не было.
В этом плане null
выполняет свою роль, ведь кто как не он говорит, что значения там нет. Но тогда что же должен был означать пустой массив? Разве не то, что куков нет?
null vs empty array
В спецификации разработчики предпочли использовать null
, что привело к некоторым последствиям, которые можно было бы избежать:
Усложнение API, которое заставляет пользователя каждый раз делать null-check
Запросы зачастую содержат хоть какой-нибудь куки, поэтому момент когда getCookies() возвратит null может произойти гораздо позже момента первого использования.
Усложнение имплементации этого метода. Если контейнер пустой, то нужно вернуть
null
(далее рассмотрим пример)
Но, конечно, не стоит считать что разработчики приняли такое решение напившись в баре. Использование null можно было оправдать, например, ради перформанса (избегаем аллоцирование пустого контейнера).
Тем не менее, это утверждение тоже можно поставить под сомнение.
Во-первых, обычно оптимизации такого уровня не требуются, если он действительно не сильно мешает производительности. Здесь нужно сделать маленькое отступление и сказать, что конкретно в этом случае это не совсем применимо, так как это спецификация и никогда не знаешь какой уровень оптимизации нужен будет разработчикам, которые разрабатывают продукт по этой спецификации.
Во-вторых, аллоцирования легко можно избежать просто создав пустой массив и возвращать его (например, какое-нибудь статическое неизменяемое поле)
Давайте посмотрим пару примеров, как использование null может испортить код
for (Cookie cookie : httpServletRequest.getCookies()) {
// NPE! // …
}
int cookiesSize = httpServletRequest.getCookies().length // NPE!
Добавляем null-check:
if (httpServletRequest.getCookies() != null)
for (Cookie cookie : httpServletRequest.getCookies()) {
// …
}
Cookie[] cookies = httpServletRequest.getCookies();
int cookiesSize = cookies == null ? 0 : cookies.length
Как и говорилось ранее, самое обидное, что этот NPE может и не появиться сразу. И разумеется, может быть сложным случайно не забыть сделать эту проверку.
Но усложнения касаются не только API, но и его имплементации. Рассмотрим как пример Jetty
Я не зря выбрал его, так как изначально он имел по сути неправильную реализацию или же лучше будет сказать имплементацию, которая не соответствует спецификации.
Коммит, который исправил ошибку
До:
return cookies == null?null:cookies.getCookies();
После:
if (cookies == null || cookies.getCookies().length == 0)
return null;
return _cookies.getCookies();
При этом в реализации была не одна точка возвращения, поэтому разработчикам везде пришлось делать такую проверку.
Справедливости ради стоит сказать, что реализация была неправильной как с точки зрения спецификации, так и со стороны нормального кода. Так как null
все равно мог вернуться и просто не было проверки на длину массива, которая если он пустой, то должен вернуть null
. Тем не менее, он хорошо показывает усложнение реализации.
Мы против!
Хотя люди, конечно же, возмущались новым изменениям, но не идти же против спецификации. Или же есть смельчаки, которые сделали это? Разумеется есть, хотя я и не знаю нарочно ли они это или по нутру хорошего кода изменили спецификацию?
Например, проект classpathx
The GNU Classpath Extensions project, aka classpathx builds free versions of Oracle's Java extension libraries, the packages in the
javax
namespace. It is a companion project of the GNU Classpath project.
У них есть скажем так "своя спецификация"
Cookie[] getCookies()
Gets all the Cookies present in the request.
Returns:
an array containing all the Cookies or an empty array if there are no cookies
Since:
2.0
Статические анализаторы
Не обойдем стороной и статические анализаторы. Они также считают что возвращать null
не лучшее решение. Например, тот же Sonar, SEI CERT Oracle Coding Standart for Java
Заключение
На этом примере мы можем увидеть как важно правильно составлять спецификации. Нужно думать как о разработчиках, которые будут его имплементировать, так и пользователях, которые будут их использовать.
В этом случае как разработчикам как и клиентам пришлось усложнять свой код.
Кто-то может сказать что в те времена, когда писалась спецификация не были так уж очевидны последствия использования null и это было обычной практикой. Да, так и есть, нам гораздо легче говорить об этом, так как мы имеем опыт предыдущих разработчиков, которые попались на этот капкан и которые нас и научили не попадаться на них. Это они были первопроходцами и поделились с нами знаниями и было бы не очень хорошо так уж сильно их осуждать, вместо благодарностей.
Тем не менее, на то это и спецификация, что она должна быть очень хорошо спроектирована, учитывая что она должна иметь обратную совместимость. Ведь проблемы с null существовали и до осознания того, что они есть.
Все что мы можем сейчас сделать - это вынести уроки из предыдущих ошибок (и своих и чужих):
Проблемы каких-то решений могут существовать и до того, как вы их осознаете
То что сейчас является обычной практикой в будущем может оказаться плохим кодом
Нужно делиться своими знаниями и опытом. Даже не ради кого-то другого, а ради себя, ведь возможно именно вашу статью прочитает будущий разработчик самого популярного фреймворка.
Язык и сообщество не стоит на месте и мы замечаем ошибки прошлого и это очень хорошо, ведь это означает, что мы стали лучше.
Speaking at a software conference in 2009, Tony Hoare apologized for inventing the null reference:[25]
I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years
imanushin
На самом деле, возврат пустого массива для случая, когда header не проставлен, не до конца корректен.
Судя по спецификации Cookie header'а, он опционален. И даже приведен пример, когда его не будет.
Если запрос требует наличия Cookie, то отсутствие header'e говорит о том, что произошло нарушение контракта: мы ожидали header, а его нет. Это не то же самое, что и пустой header.
Условно, пустая парковка — это не то же самое, что и отсутствие парковки. Дом без свободных жилых квартир — это не то же самое, что и отсутствие дома.
В большинстве случаев действительно не так важно наличие header'а, как важно наличие необходимой Cookie. Т.е. для более-менее типичного приложения скорее важно то, что в запросе присутствует определенный корректный Cookie для авторизации, трекинга и пр. Но строго говоря, это верно не для всех программ. Более того, иногда было бы полезно ловить ситуации, что сервер не обрабатывал запрос не просто потому, что header содержит некорректные данные, а потому что такого header'а вообще нет (из-за блокировщика рекламы или из-за ошибки в реализации клиентской части).
Так что возврат null в том методе полностью обоснован, так как дополнительно передается информация о том, был ли вообще такой header.
Вы правы в другом — по состоянию на 2021 год мы уже изучили, что null — слишком опасен и коварен, так что грамотнее возвращать Either/Optional/sealed class/sealed interface и так далее, ибо в этом случае пользователю API придется рассмотреть все варианты. Более того, возврат массива тоже не до конца правилен (опять-таки, по состоянию на 2021 год), так как корректнее было бы возвращать ImmutableList, чтобы дать возможность получателю безопасно сохранить результат в поле класса, так как подобная коллекция гарантирует отсутствие изменений (а заодно само API могло бы в будущем кешировать результат, если это требуется). Но это по состоянию на 2021 год, а тот метод был введен еще до всех этих рассуждений.
apploid_offical Автор
Спасибо за ваше мнение. Это первая нормальная причина использования null в этом контексте. Тем не менее, я бы не сразу согласился.
Давайте посмотрим на HttpServletRequest как Java-объект. Если мы вызываем метод getCookies(), то мы как бы подразумеваем, что в этом объекте условно есть поле cookies. И с этой точки зрения мне кажется вполне допустимо возвращение пустого массива, так как куков нет (и не важно почему, их просто нет). По сути это означает, что данный объект не содержит куки.
В их спецификации null возвращается в любом случае, если есть хедер или его нет. По сути, если куки есть, то куки, если их нет (и не важно почему), то все равно null. Так что никакой информации от возвращения null мы не получаем в любом случае.
Вот листинг из Jetty:
imanushin
Нет, так как в Java нет null safety. В новом C# оно есть, в Kotlin оно есть (частично), в Rust, а в Java нет. А потому любой аргумент может быть null'ом (если отдельно не оговорено в документации, и если реализация соответствует документации), результат вызова любой функции может быть null'ом и так далее. Кроме примитивный типов, разумеется.
А вот в чем сакральный смысл работы Jetty мне непонятно. Может быть они боялись создавать пустой массив (хотя ведь его можно было бы закешировать в статической переменной). Мне кажется, что они нарушают контракт, так как не до конца корректно поняли, что надо возвращать.
Если же они правильно реализовали контракт, то смысла в возврате null'а я тоже не вижу.
ldss
наоборот
В целом, в описанном случае какой смысл в null? У нас массив, он либо пустой, либо нет.
Если б мы возвращали обьект, тогда можно было бы о чем-то говорить, т.к. для такого случая пришлось бы создавать некий пустой/дефолтный обьект и проверять, а не пустой ли он, а это так или иначе приводит нас к доп. проверкам/странному поведению приложения
imanushin
Я же написал — для более детального разбора ошибок. Представляя себя пользователем API, мне было бы намного удобнее, если бы программа в деталях писала, что было и чего не хватило, вместо фраз в стиле "запрос некорректен".
И потом я еще добавил, что в 2021 году использовать
null
для этого я считаю опасным, лучше всего возвращать sealed interface. В этом случае пользователь API может или рассмотреть все сценарии, или пойти по короткому пути и отреагировать только на ответ с наличием Cookies (а все остальное можно свести к ответу "нет искомой cookie"). Но по состоянию на конец 90х — начало 2000х разработчики API решили сэкономить и не использовать условные visitor'ы, ради простоты кода.В любом случае, мы пошли по кругу. Мой аргумент про детализацию ошибок не является железобетонным, так что это скорее вкусовщина. И мы, вроде бы, сошлись во мнениях, что в современном коде null лучше не возвращать.
ldss
это уже вопрос обработки ошибок, имхо
У нас же ситуация простая — удалось получить куки или нет, зачем множить сущности
Maccimo
Правильно, давайте заколачивать гвозди микроскопами. И LTS версии с поддержкой sealed classes ещё нет, на минуточку.
apploid_offical Автор
Я не отрицаю что оно может быть null. Под есть я имел в виду просто наличие поля (условно и оно может быть null) (чтобы легче было представить как Java объект).
И полностью согласен с последним. Если Jetty имеет правильную реализацию (а скорее всего это так), то нет смысла возвращать null (точнее есть, но пустой массив был бы лучшей альтернативой)
dopusteam
'Условно, пустая парковка — это не то же самое, что и отсутствие парковки. Дом без свободных жилых квартир — это не то же самое, что и отсутствие дома'
Мне кажется аналогия не совсем удачная.
Если мне нужно проверить наличие конкретной cookie (или наличие конкретно авто во дворе), то мне не важно, вообще не пришли cookie или конкретно необходимой мне нет (неважно, нет парковки или машины, мне важен факт отсутствия)
Я согласен, что иногда null != empty list, но в случае с куками я не вижу кейса, где это разные ситуации.
Может Вы сможете пример привести какой-нибудь, когда необходимо отличить пустой список кук от их отсутствия?
imanushin
Я именно это и написал — в большинстве случаев это может быть неважно, так как я даже не посмотрю на этот слой, а буду настраивать условный Spring. Но вот для отладки было бы крайне полезно различать случаи, когда header вообще не проставляется, и когда отсутствует необходимая Cookie, так как в Http Client'е за это отвечают разные блоки кода (пользователь API ведь будет именно с этой стороны).
В худшем случае сервис авторизации (то есть тот, который использует класс HttpServletRequest) вернет ошибку "Auth Error", а дальше уже мне надо долго разбираться: что конкретно требовалось, где оно отсутствовало и так далее. Так как может быть все структуры заполняются, просто не передаются в Http Client. И вот для высокой детализации ошибки я бы и хотел получать максимум информации, чтобы исключить сценарий, когда каждый слой приложения теряет чуть-чуть деталей, так что потом необходимо долго изучать проблему, так как одно и то же поведение вызывается разным внешним воздействием.