- Как общаться с null в Java и не страдать
- Авторизуйтесь
- Как общаться с null в Java и не страдать
- Что такое null в Java
- Учимся избегать null-значений в современном Java. Часть 2
- Возвращайте Optional вместо null
- Содействуйте IDE с помощью аннотаций Nullable/Nonnull
- Используйте Objects.requireNonNull()
- Работа со строками
- Используйте класс Objects и библиотеку StringUtils
- Используйте StringUtils для защиты String-методов от null
- Заключение
Как общаться с null в Java и не страдать
Авторизуйтесь
Как общаться с null в Java и не страдать
Java и null неразрывно связаны. Трудно найти Java-программиста, который не сталкивался с NullPointerException . Если даже автор понятия нулевого указателя признал его «ошибкой на миллиард долларов», почему он сохранился в Java? null присутствует в Java уже давно, и я уверен, что разработчики языка знают, что он создает больше проблем, чем решает. Это удивительно, ведь философия Java — делать вещи как можно более простыми. Если разработчики отказались от указателей, перегрузки операторов и множественного наследования, то почему они оставили null ? Я не знаю ответа на этот вопрос. Однако не имеет значения, насколько много критики идет в адрес null в Java, нам придется с этим смириться. Вместо того, чтобы жаловаться, давайте лучше научимся правильно его использовать. Если быть недостаточно внимательным при использовании null , Java заставит вас страдать с помощью ужасного java.lang.NullPointerException . Наиболее частая причина NullPointerException — недостаточное понимание тонкостей использования null . Давайте вспомним самые важные вещи о нем в Java.
Что такое null в Java
Как мы уже выяснили, null очень важен в Java. Изначально он служил, чтобы обозначить отсутствие чего-либо, например, пользователя, ресурса и т. п. Но уже через год выяснилось, что он приносит много проблем. В этой статье мы рассмотрим основные вещи, которые следует знать о нулевом указателе в Java, чтобы свести к минимуму проверки на null и избежать неприятных NullPointerException .
1. В первую очередь, null — это ключевое слово в Java, как public , static или final . Оно регистрозависимо, поэтому вы не сможете написать Null или NULL , компилятор этого не поймет и выдаст ошибку:
Эта проблема часто возникает у программистов, которые переходят на Java с других языков, но с современными средами разработки это несущественно. Такие IDE, как Eclipse или Netbeans, исправляют эти ошибки, пока вы набираете код. Но во времена Блокнота, Vim или Emacs это было серьезной проблемой, которая отнимала много времени.
2. Так же, как и любой примитивный тип имеет значение по умолчанию (0 у int , false у boolean ), null — значение по умолчанию любого ссылочного типа, а значит, и для любого объекта. Если вы объявляете булеву переменную, ей присваивается значение false . Если вы объявляете ссылочную переменную, ей присваивается значение null , вне зависимости от области видимости и модификаторов доступа. Единственное, компилятор предупредит о попытке использовать неинициализированную локальную переменную. Для того, чтобы убедиться в этом, вы можете создать ссылочную переменную, не инициализируя ее, и вывести ее на экран:
Это справедливо как для статических, так и для нестатических переменных. В данном случае мы объявили myObj как статическую переменную для того, чтобы ее можно было использовать в статическом методе main .
4–5 декабря, Онлайн, Беcплатно
3. Несмотря на распространенное мнение, null не является ни объектом, ни типом. Это просто специальное значение, которое может быть присвоено любому ссылочному типу. Кроме того, вы также можете привести null к любому ссылочному типу:
Как видите, приведение null к ссылочному типу не вызывает ошибки ни при компиляции, ни при запуске. Также при запуске не будет NullPointerException , несмотря на распространенное заблуждение.
4. null может быть присвоен только переменной ссылочного типа. Примитивным типам — int , double , float или boolean — значение null присвоить нельзя. Компилятор не допустит этого и выдаст ошибку:
Итак, попытка присвоения значения null примитивному типу — ошибка времени компиляции, но вы можете присвоить null типу-обертке, а затем присвоить это значение соответствуему примитиву. Компилятор ругаться не будет, но при выполнении кода будет брошено NullPointerException . Это происходит из-за автоматического заворачивания (autoboxing) в Java
5. Любой объект класса-обертки со значением null кинет NullPointerException при разворачивании (unboxing). Некоторые программисты думают, что обертка автоматически присвоит примитиву значение по умолчанию (0 для int , false для boolean и т. д.), но это не так:
Если вы запустите этот код, вы увидите Exception in thread «main» java.lang.NullPointerException в консоли. Это часто случается при работе с HashMap с ключами типа Integer . Код ниже сломается, как только вы его запустите:
Этот код выглядит простым и понятным. Мы ищем, сколько каждое число встречается в массиве, это классический способ поиска дубликатов в массиве в Java. Мы берем предыдущее значение количества, инкрементируем его и кладем обратно в HashMap . Мы полагаем, что Integer позаботится о том, чтобы вернуть значение по умолчанию для int , однако если числа нет в HashMap , метод get() вернет null , а не 0. И при оборачивании выбросит NullPoinerException . Представьте, что этот код завернут в условие и недостаточно протестирован. Как только вы его запустите на продакшен – УПС!
6. Оператор instanceof вернет false , будучи примененным к переменной со значением null или к литералу null :
Это важное свойство оператора instanceof , которое делает его полезным при приведении типов.
7. Возможно, вы уже знаете, что если вызвать нестатический метод по ссылке со значением null , результатом будет NullPointerException . Но зато вы можете вызвать по ней статический метод класса:
Результат выполнения этого кода:
8. Вы можете передавать null в любой метод, который принимает ссылочный тип, например, public void print(Object obj) может быть вызван так: print(null) . С точки зрения компилятора ошибки здесь нет, но поведение такого кода целиком зависит от реализации метода. Безопасный метод не кидает NullPointerException в этом случае, а тихо завершает работу. Если бизнес-логика позволяет, лучше писать безопасные методы.
9. Вы можете сравнивать null , используя оператор == («равно») и != («не равно»), но не с арифметическими или логическими операторами (такими как «больше» или «меньше»). В отличие от SQL, в Java null == null вернет true :
Вывод этого кода:
Вот и все, что надо знать о null в Java. При наличии небольшого опыта и с помощью простых приемов вы можете сделать свой код безопасным. Поскольку null может рассматриваться как пустая или неинициализированная переменная, важно документировать поведение метода при получении null . Помните, что любая созданная и не проинициализированная переменная имеет по умолчанию значение null и что вы не можете вызвать метод объекта или обратиться к его полю, используя null .
Источник
Учимся избегать null-значений в современном Java. Часть 2
В предыдущей статье мы разобрали, почему в некоторых случаях null оказывается необходимым злом, а также узнали, что есть правильные и ошибочные варианты его использования. В текущей же статье я расскажу, как наиболее безопасно работать с null -значениями и приведу некоторые варианты решений, используемых нами в проекте Columna для того, чтобы избежать ошибок и применить изящную обработку null .
Возвращайте Optional вместо null
Хоть в предыдущей статье я и убеждал вас в уместности null для некоторых сценариев, в этой я буду напротив говорить, что по возможности его все же лучше избегать. При каждом добавлении в базу кода нового метода, способного вернуть пустое значение, вам следует стараться использовать вместо null тип Optional. Этот тип был введен, чтобы разработчики могли указывать на возможность возвращения методом пустого значения, что ранее было недоступно (либо требовало аннотаций, о которых я расскажу ниже). Существует два вида объектов Optional : содержащие значение и без него. Первые изначально создаются со значением, вторые же просто являются статическими одиночками. Структурно типичный случай их использования выглядит так:
Чтобы увидеть, почему сигнатура с Optional предпочтительней, представьте следующий метод:
Из этой сигнатуры понятно лишь, что она возвращает объект типа Admission . При этом, как уже говорилось, null соответствует любому типу. У нас нет иной возможности узнать, возвращает ли данный метод null , кроме как проанализировать его реализацию. В противоположность приведенному примеру, метод, возвращающий Optional , будет выглядеть так:
Поскольку смысл использовать Optional есть только в случае возможного возврата пустого значения, значит метод такое значение вернуть может. Поэтому при каждом возвращении Optional методом вы знаете, что есть вероятность получения пустого значения и его нужно должным образом обработать.
Возвращаемое значение Optional обрабатывается аналогично null : вы проверяете, передано ли значение, и, если да — выполняете с ним определенные действия. Вызов метода, возвращающего null , как правило, выглядит так:
Со значением Optional соответствующий код будет выглядеть так:
В то время как можно легко забыть выполнить проверку на null в первом примере, тип Optional безопаснее и делает очевидным возможное отсутствие возвращаемого значения. Если вы вызовите get() для пустого значения Optional , то получите исключение, как и в случае вызова любого метода для значения null . В самых простых случаях перед вызовом get() всегда нужно вызывать isPresent() , однако API предлагает и другие альтернативы, где при отсутствии значения может возвращаться его предустановленный вариант.
Обратите внимание, что тип Optional резонно использовать только для возвращаемых значений. Например, в примере ниже присваивание Optional в цикле ничем не лучше null . На деле использование null для начального присваивания будет даже более изящным, и у вашей IDE не должно возникнуть сложностей в обнаружении упущенной проверки на null , когда переменная используется в том же методе, где была объявлена.
Однако если бы вместо этого у нас был метод, возвращающий активную госпитализацию, то предпочтительнее было бы использовать Optional :
Можно ли использовать Optional для параметров?
SonarLint отвечает “Нет”, поскольку лучше предоставить действительное значение, чем Optional , которое может его иметь или нет. Ведь иначе нам придется проверять в методе оба случая, а также убеждаться, что никто не передал вместо него null. Смешивать же null и Optional вообще не стоит, иначе смысл в использовании последнего полностью исчезнет.
Никогда не делайте следующее, равно как не предоставляйте null в качестве аргумента параметра с Optional значением:
Если вы сделаете нечто подобное, то также получите NPE (исключение нулевого указателя).
Если у вас есть некое значение, которое, вероятно, содержит null , и вы хотите преобразовать его в Optional , то вам нужно вызвать вместо этого Optional.ofNullable() , который вернет обернутое в Optional значение, если аргумент не является null . В противном случае он вернет пустой Optional . Если же вам нужно выполнить обратную операцию, т.е. преобразовать, вероятно, пустое значение Optional в null или значение, которое оно содержит, используйте orElse(null) , который при отсутствии значения возвращает предоставленный аргумент.
Содействуйте IDE с помощью аннотаций Nullable/Nonnull
Для большинства баз кода полное отсутствие null в передаче данных является утопией. Еще одним способом прояснить ваши намерения и одновременно помочь системе типов помочь вам — это использовать для параметров и возвращаемых значений аннотации, которые указывают, где стоит ожидать null , а где нет, а также где его использование предполагается. Такую возможность предлагают рзаные фреймворки, но основная идея одна: параметр можно снабдить либо тегом Nonnull , сообщая, что значение null для него ожидать не нужно, либо Nullable , указывая обратное. Добавление тегов допустимо также для полей и возвращаемых значений.
Вот один из примеров подобной сигнатуры метода:
Сейчас эта сигнатура показывает, что метод не предполагает вызов с параметром Patient , содержащим null , и если такой вызов произойдет, IDE выдаст предупреждение. При этом мы также проинформированы, что данный метод может вернуть null , и вы аналогичным образом получите предупреждение при использовании этого возвращаемого значения метода.
Если новым методам в возвращаемых значениях следует использовать Optional вместо null , то выполнение обширного рефакторинга существующих методов обычно в ходе разработки функционала не рассматривается. Тем не менее, когда вы пишите код, то простое добавление тега после каждой проверки, допускает ли значение null , окажет помощь всем, кто будет использовать этот код в будущем, и поможет избежать NPE.
Используйте Objects.requireNonNull()
Аннотации имеют значение только во время компиляции и не влияют на поведение программы при выполнении. Если вам нужно, чтобы при выполнении метод не вызывался с null -аргументом, то следует использовать метод Objects.requireNonNull (T obj, строка сообщения). Объекты — это статический служебный класс для выполнения основных операций вроде equals , hashCode и toString , защищенным от null способом.
Чтобы оградить метод от нулевых аргументов, передайте в метод requireNonNull() соответствующие параметры вместе с описательным текстом ошибки. Если параметр окажется null , это вызовет NPE с указанным вами сообщением об ошибке. В противном случае данный метод просто вернет параметр. Обычно его используют в конструкторах, как показано ниже:
Здесь у вас может возникнуть вопрос, заключается ли причина избегания нулевого параметра в том, что он может ненамеренно привести к NPE. Не будет ли лучше выбросить NPE намеренно до того, как это произойдет?
Скорее всего, это одно и то же, и с позиции пользователя выглядеть это будет тоже одинаково — в виде появления ошибки. А вот при диагностировании причины сбоя в дальнейшем, последний вариант окажется гораздо предпочтительнее. Проблема с NPE в том, что они не сообщают, какая переменная оказалась null, а просто предоставляют номер строки. В строке, где для объектов вызывается несколько методов, может быть сложно определить, в каком из них причина.
Предположим, вы получаете NPE в следующей строке кода:
Какая из переменных была null ? И bar , и baz , и qux могли оказаться нулевыми. Если qux не была null , то объект, который она возвращает, мог. Потребуется потратить какое-то время на выяснение, какой из этих сценариев оказался виновником.
Выбрасывание собственного исключения для пустого значения параметра будет тут же показывать в лог-файлах, где искать проблему, потому что на нее будет указывать ваше собственное сообщение об ошибке.
Работа со строками
Я часто вижу баги там, где логике при генерации строк не удается учесть нулевые значения, в результате чего среди строк пользовательского интерфейса появляется null . Причина такого коварного поведения в том, что в Java при конкатенации строк неожиданное нулевое значение не ведет к NPE до тех пор, пока для него явно не вызываются методы. Если мы заглянем внутрь JRE, то выясним, что null-значения часто явно обрабатываются и преобразуются в null — значения. Каждый раз, когда мы используем сокращение “ + ”, генерируется StringBuilder и используется для создания объединенного значения. Это означает, что правосторонние значения присваиваний s3 и s4 по факту эквивалентны:
В обоих случаях получится строка hello null . Конкатенацию также можно выполнить при помощи s1.concat(s2) , и это, что интересно, будет вызывать NPE всякий раз, когда s2 будет null . Тем не менее метод concat не является предпочтительным несмотря на то, что справляется лучше StringBuilder при конкатенации всего нескольких строк. В связи с этим мы, как правило, будем обнаруживать null -строки лишь в UI.
Используйте класс Objects и библиотеку StringUtils
Ошибки нередко прокрадываются в базу кода, когда у нас нет ясного понимания того, что код делает или почему он это делает. Здесь важно удобство в обслуживании. Я часто сталкиваюсь с запутанным кодом создания строки. По какой-то причине люди склонны увлекаться тернарными выражениями, помещая их в другие выражения или вкладывая друг в друга. Это очень эффективно усложняет чтение и понимание простой логики.
Тернарный оператор часто используется для возвращения предустановленного значения, когда переменная содержит null . Ниже приведен пример этого, взятый прямо из нашей базы кода Columna:
Примечательно, что это не самый сложный для анализа код, но я думаю, что его можно написать гораздо лучше. В C# существует так называемый оператор объединения с неопределенным значением, относящийся к тернарному оператору, явно проверяющему значение на null . Вот пример:
Здесь левое значение оператора присваивается, если оно не null , в противном случае используется правая сторона. Несмотря на то, что в Java недостает подобного оператора, с той же целью можно использовать встроенный класс Objects , описанный выше. Метод toString(Object o, String s) класса Objects возвращает значение toString() первого объекта, если этот объект не null , в противном случае он возвращает указанное строковое значение.
Не так складно, как в примере с C#, но, на мой взгляд, намного надежнее, чем исходный пример, а значит и шанс появления ошибок при работе с кодом уже существенно меньше.
Используйте StringUtils для защиты String-методов от null
Библиотека StringUtils упрощает обработку null и пустых строк. В API это описывается как: “Защищенные от null операции со String.” Эта библиотека содержит много стандартных строковых операций со встроенными проверками на null . При ее использовании база кода станет менее громоздкой, так как вам не потребуется реализовывать весь рутинный код проверок на null , и читать условные выражения станет легче. При этом база кода также станет более единообразной, и что более важно, код станет безопаснее, так как вы уже ненароком не забудете реализовать проверки нулевых значений. В частности, такая агрессивная защита выражений конкатенации с условными инструкциями на основе метода isBlank(s) гарантирует, что в строки уже не проскользнут нежелательные “null”-значения.
Заключение
Вкратце содержание рассмотренной серии статей можно выразить так:
Источник