Введение: «а если всё-таки NPE?»
«Kotlin — null safe, значит NPE в нём невозможны?» — вопрос, который звучит на каждом втором собеседовании по Kotlin. И правильный ответ на него — «нет, не невозможны». Отсюда и начинается настоящий разбор.
Null safety — одна из главных причин перехода на Kotlin: вместо миллиардной ошибки в рантайме проверка переезжает в компилятор. Но у фичи есть границы: оператор !!, платформенные типы из Java-кода и проблемы инициализации — всё это реальные источники NPE. Интервьюер, который копнул глубже первого вопроса, обязательно до них доберётся.
Разберём по порядку: типы, инструменты, границы null safety и формулировки для уверенного ответа.
Nullable и non-nullable типы
Основа null safety — система типов. В Java любая ссылка может быть null. В Kotlin тип делится на два: String — не может быть null, String? — может:
var a: String = "abc"
// a = null — ошибка компиляции: null не может быть значением типа String
var b: String? = "abc"
b = null // окКлючевое слово — гарантия компилятора. Пока переменная объявлена как String, компилятор следит, что ей никогда не присвоят null, и позволяет обращаться к её членам напрямую — a.length безопасен. А вот b.length уже не скомпилируется: компилятор заставит обработать nullable-значение.
Именно это отличие от Java — главный пункт ответа: в Kotlin nullability — часть типа, а не случайность рантайма. Проверка происходит на этапе компиляции, а не в момент падения приложения. Официальная документация по null safety прямо называет это способом «поймать проблемы, связанные с null, на этапе компиляции, а не в рантайме».
Следствие, о котором стоит сказать вслух: nullable-тип — это не «какой-то тип с вопросом», а явная декларация намерения. String? говорит читателю кода: здесь может быть null, и это предусмотрено. Non-null тип говорит: значение гарантировано.
Инструменты: ?., ?: и !!
Когда переменная nullable, её нужно разыменовать безопасно. Первый способ — проверка через if с умным приведением типов:
val b: String? = null
val l = if (b != null) b.length else -1После проверки b != null компилятор сам приводит String? к String — это называется smart cast. Но для коротких выражений проверка громоздкая, поэтому есть операторы.
Безопасный вызов ?. — вместо NPE возвращает null:
val a: String? = "Kotlin"
val b: String? = null
println(a?.length) // 6
println(b?.length) // null, а не NPEТип результата b?.length — Int?, то есть тоже nullable. ?. отлично работает в цепочках: bob?.department?.head?.name вернёт null, если любой из элементов цепочки равен null.
Элвис-оператор ?: — значение по умолчанию, если слева null:
val l = b?.length ?: 0Правая часть вычисляется только если слева null. А поскольку в Kotlin throw и return — выражения, их можно ставить справа:
fun findName(): String? {
val name = getParent() ?: return null
val title = getTitle() ?: throw IllegalArgumentException("title expected")
return title
}Это идиоматичный способ проверки аргументов — элвис с throw работает как валидатор.
Оператор !! — снять nullable принудительно:
val text: String? = null
val length = text!!.length // NullPointerException!! говорит: «я уверен, что тут не null, компилятор, не приставай». Если уверенность не подтвердилась — NPE в этой строке. На собеседовании стоит сразу добавлять: !! — это явный источник NPE, использовать его можно только когда гарантия очевидна, и это скорее исключение, чем правило.
Частая ошибка кандидата: «ну у нас в проекте всё с !!». Это красный флаг для интервьюера: значит, null safety используется как декорация, а не как инструмент. Правильная идиома — ?. и ?: везде, где значение может быть null, и явная обработка на границе системы.
Smart cast, let и безопасные касты
Кроме операторов, в Kotlin есть ещё три инструмента, которые любят проверять.
Smart cast — компилятор сам приводит тип там, где null-проверка очевидна:
if (b != null && b.length > 0) {
println("Длина: ${b.length}") // b уже String
}Условие может быть сложным, но компилятор обязан убедиться, что значение не изменилось между проверкой и использованием. Для var, который меняется из другого места, smart cast не сработает — и это тоже вопрос с собеседований.
let — выполнить блок только если значение не null:
user?.let { user ->
showProfile(user) // выполнится только если user != null
}Комбинация ?.let — идиома «если не null, сделай что-то» без вложенных проверок.
Безопасный каст as? — вместо ClassCastException возвращает null:
val a: Any = "Hello"
val aInt: Int? = a as? Int // null, исключения нет
val aString: String? = a as? String // "Hello"Для коллекций есть filterNotNull() — отфильтровать List<String?> до List<String> одним вызовом.
Когда NPE всё-таки случится
Самая важная часть для собеседования: null safety не отменяет NPE, а сужает его причины до списка. Документация перечисляет их явно:
- Оператор
!!— принудительное снятие nullability на null-значении. - Явный
throw NullPointerException()— ну тут всё честно. - Проблемы инициализации — когда объект используется до полной инициализации: «утечка this» из конструктора или вызов open-метода из конструктора суперкласса, когда состояние наследника ещё не готово.
- Java-интероп — доступ к членам null-значения платформенного типа; добавление
nullв generic-коллекции; другие внешние причины.
К этому списку примыкает lateinit. Свойство с lateinit объявляется non-null, но инициализируется позже. Если прочитать его до инициализации — будет не NPE, а отдельное UninitializedPropertyAccessException. Это тоже спрашивают, и разницу стоит проговаривать точно.
!!на null-значении — NPE по вашему явному решению.- Платформенный тип из Java-кода вернул null — компилятор не мог это проверить.
- «Утечка this» или open-метод в конструкторе суперкласса — проблема инициализации.
- lateinit прочитан до инициализации — UninitializedPropertyAccessException.
Платформенные типы: граница с Java
Отдельный разговор — интероп. Java-код не знает про nullability, поэтому при вызове из Kotlin типы без аннотаций становятся платформенными: String! — «String, но я не знаю, может ли быть null».
// Java: public String getName() — может вернуть null
val name: String = javaObject.name // компилируется! но если null — NPE в рантаймеКомпилятор позволяет присвоить платформенное значение и в String, и в String? — выбор за вами. Ошибка возникает, когда выбрали String, а Java вернул null: NPE произойдёт уже в Kotlin-коде, при обращении к значению.
Правило для прод-кода: проверять nullability на границе — там, где Kotlin вызывает Java. Аннотации вроде @Nullable/@NotNull (или @NonNull из androidx) частично решают проблему: типы с аннотациями компилятор трактует правильно, а вот без них — только платформенный !. Плюс generics: Java может положить null в MutableList<String>, хотя для Kotlin это non-null тип — отсюда ещё один источник проблем с дженериками, который перечислен в документации.
Как отвечать про платформенные типы
«Платформенный тип — это тип из Java без информации о nullability. Компилятор не проверяет его, поэтому ошибку можно допустить на нашей стороне: если Java вернул null, а мы объявили non-null тип, NPE случится в рантайме. Поэтому на границе с Java я проверяю значения явно».
Итоги
Коротко по главному:
- Система типов —
Stringне может бытьnull,String?может; проверка на этапе компиляции. - Инструменты —
?.для безопасного доступа,?:для значения по умолчанию,!!для явного снятия,as?для безопасных кастов, smart cast иletдля работы с nullable. - Границы — NPE возможен:
!!, платформенные типы из Java, проблемы инициализации и явный throw.
Главное, что нужно запомнить
Null safety не убирает NPE — он сужает его до четырёх задокументированных причин. Уверенно называете их и умеете работать с платформенными типами — тема закрыта.
Если на собеседовании вопросы про null safety заканчиваются на «ну, это же оператор с вопросиком», а про платформенные типы вы слышите впервые — не откладывайте разбор на потом. Напишите в Telegram, разберём вашу ситуацию и составим план, как закрыть пробелы в Kotlin до следующего интервью.