Null safety: ?., ?:, !!, lateinit и платформенные типы
«Kotlin избавляет от NullPointerException» — фраза, после которой хороший интервьюер обязательно спросит: «а в каких случаях NPE всё-таки прилетит?»
Null safety в Kotlin — это разделение типов на
TиT?на уровне системы типов. Компилятор не даёт обратиться к nullable-значению без явной проверки, поэтому большинство NPE ловится ещё до запуска.
Вопрос 1: «Расскажите про операторы null safety»
Что проверяет интервьюер
Не перечисление символов, а умение выбрать оператор осознанно — особенно отношение к !!.
Развёрнутый ответ
| Оператор | Что делает | Когда уместен |
?. | вызов, если не null, иначе выражение равно null | цепочки обращений |
?: (Elvis) | значение по умолчанию для null | подстановка запасного значения или ранний выход |
!! | бросает NPE, если null | почти никогда; только там, где null означает баг в коде |
as? | безопасное приведение, null вместо ClassCastException | работа с типами из внешних источников |
?.let { } | выполнить блок только для не-null значения | когда нужно тело из нескольких строк |
data class User(val profile: Profile?)
data class Profile(val city: String?)
fun cityOf(user: User?): String =
user?.profile?.city ?: "не указан"
// Elvis отлично сочетается с return и throw
fun requireCity(user: User?): String {
val city = user?.profile?.city ?: return "неизвестно"
return city.uppercase()
}
// безопасное приведение
val text = payload as? String ?: "не строка"
Про !! стоит сказать отдельно и правильным тоном: это не «плохо», это явное утверждение разработчика «здесь null невозможен». Если утверждение неверно, приложение падает сразу и в понятном месте, а не через десять экранов. Но в 90 % случаев !! — признак того, что тип стоило спроектировать иначе.
Вопрос 2: «Можно ли всё-таки получить NPE в Kotlin?»
Ответ: да, и способов несколько — этим вопросом отделяют выученное от понятого.
- Оператор
!!— самый очевидный. - Платформенные типы из Java. Когда Kotlin вызывает Java-код, компилятор не знает, вернётся ли null, и присваивает результату «платформенный тип»
String!. Проверок он не требует, но NPE в рантайме возможен. Спасение — аннотации@Nullable/@NonNullв Java-коде и явное объявление типа как nullable на стороне Kotlin. lateinitдо инициализации —UninitializedPropertyAccessException, наследник рантайм-исключения того же класса проблем.- Утечка
thisиз конструктора: открытый метод, вызванный в конструкторе базового класса, отработает раньше, чем инициализируются поля наследника, и увидит их равными null.
// Java-метод, о nullability которого компилятор ничего не знает
val name: String = javaApi.getUserName() // платформенный тип, проверки нет
println(name.length) // NPE, если Java вернула null
// защищаемся явным типом
val safeName: String? = javaApi.getUserName()
println(safeName?.length ?: 0)
Вопрос 3: «lateinit или nullable — что выбрать?»
lateinit var говорит компилятору: «значение появится позже, но к моменту чтения оно точно будет». Это удобно в Android, где binding и внедрённые зависимости появляются не в конструкторе, а в onCreate или через DI.
class MainActivity : AppCompatActivity() {
@Inject lateinit var analytics: Analytics
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
private fun log() {
if (::analytics.isInitialized) analytics.track("open")
}
}
Ограничения, о которых спрашивают: lateinit нельзя применить к val, к примитивным типам (Int, Boolean) и к nullable-типу. Проверить состояние можно через ::property.isInitialized.
Правило выбора простое: если отсутствие значения — это нормальная ситуация в логике, тип должен быть nullable. Если отсутствие значения означает ошибку в коде (забыли внедрить зависимость, обратились до onCreate), уместен lateinit.
Типичные ошибки кандидатов
- Расставляют
!!«чтобы компилятор замолчал» — и переносят проблему из компиляции в рантайм пользователя. - Не знают про платформенные типы и уверенно заявляют, что NPE в Kotlin невозможен.
- Не могут объяснить, почему smart cast иногда «не срабатывает». Причина: компилятор гарантирует неизменность только для
valи локальных переменных. Дляvar-свойства класса (особенно открытого или из другого модуля) значение теоретически может измениться между проверкой и использованием — поэтому нужна локальная копия или?.let.
class Screen {
var title: String? = null
fun show() {
if (title != null) {
// println(title.length) // ошибка: smart cast невозможен для var-свойства
}
title?.let { println(it.length) } // так работает
}
}
Как ответить кратко (20–30 секунд)
«Kotlin разделяет
TиT?на уровне типов, поэтому большинство NPE ловится компилятором. Основные инструменты:?.для цепочек, Elvis?:для значения по умолчанию или раннего выхода,as?для безопасного приведения,?.letдля блока.!!использую редко — это явное утверждение «здесь null невозможен». NPE всё-таки возможен: через!!, через платформенные типы из Java, через обращение к неинициализированномуlateinit. Выбор междуlateinitи nullable делаю по смыслу: отсутствие значения — нормальная ситуация, значит nullable; признак бага — значитlateinit.»