Что такое NPE
Exceptions (или исключения) в программировании позволяют описать проблему, если в программе что-то пошло не так. Как правило это описание можно увидеть в логах, при возникновении ошибок. NullPointerException – это ошибка, которая возникает, когда используемый объект не инициализирован. Дословно она переводится как “исключение нулевого указателя”. Еще ее коротко называют NPE.
Что такое null
Зачастую программы крашатся, когда на определенном этапе при обращении к переменной, в ней оказывается null без всякой обработки – то есть пустота. Ключевое слово null было создано для обозначения отсутствия объекта.
Из-за чего может падать программа
Сначала расскажу в каких ситуациях это может происходить в продуктовой разработке. Представим, что мы делаем какое-то приложение, где есть экран с контактами пользователей. В коде мы создали класс Contact с полями Имя, Номер телефона, Место работы. Данные этих контактов приходят из интернета в виде объектов в специальном текстовом формате JSON.
Если не углубляться в процесс парсинга, можно сказать что объекты из строки форматируются в создаваемые объекты нашего класса-шаблона Contact. После чего для отображения конкретного контакта на экране приложения мы обращаемся к требуемым полям созданного экземпляра. Не важно откуда на сервере появляются эти объекты с контактами, но может сложиться такая ситуация, при получении данных в приложение, что в одном из контактов поле с местом работы прийдет null. И приложение падает с ошибкой NullPointerException. Потому что в модели (в классе) ожидается обычная строка, а сервер, не предупреждая, присылает null.
nullable-типы
В разных языках есть определенные конструкции для отлавливания таких ошибок, но в Kotlin есть короткое изящное решение. Забегая вперед – это nullable-типы и применение безопасного вызова. Теперь обо всем подробнее.
В языке существуют так называемые nullable-типы. Такой тип может содержать какое-то значение, а может содержать null. То есть в отличие от переменной стандартного типа, мы явно говорим, что “эта переменная может принимать в себя null”. В случае со стандартной переменной точно известно, что она может хранить только значение соответствующего типа. Еще ключевое слово null можно использовать при инициализации переменной. Так мы говорим, что сейчас в этой переменной присвоить нечего и данные могут прийти позже, по ходу работы программы.
Вот как это выглядит. Объявим переменную с каким-либо базовым типом, например, String и присвоим ей null. Ошибка. null нельзя присвоить в переменную “необнуляемого” типа, причина тому жесткое разделение на обычные и обнуляемые типы в языке. Nullable типы обозначаются знаком вопроса после написания типа. Теперь мы знаем, что эта переменная может хранить или null, или какое-то значение в данном случае типа String. Для сравнения объявим ниже обычную строковую переменную.
val nullableString: String? = null
val nonNullableString: String = "some string"
Оператор безопасного вызова – ?.
Далее можно обратиться к переменным, например, вызвать поле класса String – length. Оно хранить в себе количество символов в определенной строке.
val nullableString: String? = null
val nonNullableString: String = "some string"
println(nullableString.length)
println(nonNullableString.length)
На примере видно, что переменная nullableString при обращении к length подсвечивает ошибку. Такой код не скомпилируется. Потому что теперь Kotlin всегда будет предупреждать о вероятности того, что здесь может быть null – еще на этапе разработки. То есть до компиляции. Поэтому при обращении к свойствам или методам для nullable переменной, необходимо использовать оператор безопасного вызова – знак вопроса с точкой.
println(nonNullable?.length)
Этот оператор совершает проверку значения на null слева от себя и если оно не null, то выполняет код. Если значение null, то не происходит ничего. И это одна из ценных фичей, потому что иначе в этом месте можно получить исключение NullPointerException.
По сути это что-то вроде сокращенного условного выражения if-else, только в нем нет условия else. Потому что мы можем принудительно сделать проверку на null через условие и для компилятора этого будет достаточно, чтобы не требовать использование оператора безопасного вызова. Например, развернуто можно написать так.
val nullableString: String? = null
if (nullableString != null) {
println(nullableString.length)
} else {
println("Переменная хранит null")
}
Ок. Объявим еще одну обнуляемую переменную. Вместо явного null, проинициализируем функцией readLine(). Функция, напомню, считывает данные с клавиатуры в консоли. Так вот, переменная, в которую будет сохраняться полученное значение должна быть обнуляемой. Если отправить пустую строку, придет просто строка с нулевым количеством символов (это не null). Но при определенных условиях эта функция может возвращать null, например, при чтении файла.
Вообщем да, эта функция требует сохранять значение в переменную с nullable типом. Кроме того можно провалиться в декларацию функции и увидеть, что тип возвращаемого значения у функции readLine() – String?. Это обязывает нас все дальнейшие взаимодействия с полученным значением делать безопасными.
Оператор “элвис” – ?:
Продолжим. Вновь обратим внимание на переменную с названием nullableString. И попробуем обратиться к полю length, чтобы попытаться узнать количество символов в строке. Но результат будем сохранять в целочисленную переменную типа Int. Именно такого типа хранит значение свойства length.
val nullableString: String? = null
val length: Int = nullable?.length
Инициализировать не обнуляемую переменную нельзя и при использовании оператора безопасного вызова. Так как потенциально в нее может попасть null значение. Но как быть, если мы по условиям программы не можем позволить себе хранить в переменной-контейнере null? Ну или просто хотим сократить количество нулабельных переменных. Мы можем сделать проверку на null прямо в строке и если значение будет null, присвоить переменной какое-либо значение по умолчанию. Здесь снова можно проследить аналогию с if-else, но на этот раз ветка else здесь будет присутствовать и возвращать значение определенного типа. Это можно сделать с помощью так называемного “элвис” оператора ?: – название следует из сходства с эмодзи Элвис.
val nullableString: String? = null
val length: Int = nullableString?.length ?: 0
Работает оператор просто. Слева от себя он проверяет значение на null и если оно таковым оказывается, отдает значение справа от себя, которое точно не будет null и не вызовет ошибки. Вот так на примере readLine() можно присваивать значения в ненулабельную переменную.
val someString: String = readLine() ?: ""
Оператор утверждения “это не null” – !!
Далее. Есть еще один оператор под названием “Оператор утверждения Это не null”. Обозначается двумя восклицательными знаками. С помощью него можно можно преобразовать nullable-типы в стандартный тип. Он работает без какой-либо проверки на наличие в переменной null, поэтому в случае если там действительно окажется null программа выбросит исключение NullPointerException. В продуктовой разработке рекомендуется использовать крайне осторожно и только в тех местах, где есть обоснованная уверенность, что null не придет. Потому что по сути мы сознательно отказываемся от проверки на null и подвергаем риску работу программы. Вот небольшой пример для демонстрации.
val nullableString: String? = null
val length: Int = nullableString!!.length
Здесь утрированно показана nullable переменная типа String?. Которая по определению может хранить или null или строковое значение. И мы намеренно присвоили null. Далее производим обращение к этой переменной. От нас требуется проставить или оператор безопасного вызова или оператор утверждения. Пишем его, подразумевая, что здесь не может быть null. При запуске программы получаем то самое NPE исключение.
Для тех, кто собрался стать Android-разработчиком
Пошаговая
схема
Описание процесса обучения от основ Kotlin до Android-разработчика
Бесплатные
уроки
Авторский бесплатный курс по основам языка программирования Kotlin
Обучающий
бот
Тренажер и самоучитель по Котлин – бесплатные тесты и практика