NullPointerException — самая известная ошибка Java: значение оказалось null там, где его никто не ждал, и выяснилось это в проде. Kotlin решает проблему радикально: возможность null записана в типе, и компилятор не даст обратиться к значению, не разобравшись с null. Это главное, ради чего команды переходят на Kotlin.
Два типа вместо одного
var a: String = "text"
// a = null // не компилируется
var b: String? = "text"
b = null // ок: тип допускает null
String и String? — разные типы. У non-null типа null не бывает вообще; у nullable (?) — компилятор требует обработать null перед использованием:
// b.length // не компилируется: b может быть null
val len = if (b != null) b.length else 0 // ок: проверка, дальше smart cast
После явной проверки b != null компилятор сам уточняет тип до String — это тот же smart cast, что и с is.
Операторы работы с null
Безопасный вызов ?. — «вызови, если не null, иначе верни null»:
val city: String? = user?.address?.city
Цепочка не упадёт: если любой шаг null — результат null. Сравните с лестницей if-ов или цепочкой Optional.map в Java.
Элвис ?: — «значение по умолчанию, если слева null»:
val port = config.port ?: 5432
val user = repo.find(id) ?: throw NotFoundException(id)
Справа от ?: может стоять и throw, и return — удобно для ранних выходов.
let для действий над не-null значением:
order.trackingNumber?.let { notifyCarrier(it) }
Блок выполнится только если значение не null; внутри it — уже non-null.
!! — «я уверен, падай если нет». Превращает T? в T, бросая NPE при null. Это осознанный аварийный люк: каждый !! в коде — место, где вы вернули себе java-поведение. В прикладном коде почти всегда есть способ лучше.
lateinit: поля, которые заполнят позже
lateinit var dataSource: DataSource
Для полей, которые инициализируются после конструктора (DI-фреймворком, в setUp теста). Обращение до инициализации даёт понятную ошибку вместо NPE. В Spring-коде на Kotlin встречается, но конструкторная инъекция делает его почти ненужным.
Platform types: граница с Java
Java-типы не знают про nullability, поэтому значение из Java-кода приходит в Kotlin как platform type (String! в сообщениях компилятора): компилятор не проверяет его на null — ответственность на вас, как в Java.
val name = javaUser.getName() // тип String! — может быть null, компилятор молчит
Что с этим делать:
- сразу фиксировать тип явно:
val name: String? = javaUser.getName()— и дальше компилятор следит; - в своём Java-коде ставить аннотации
@Nullable/@NotNull— Kotlin их читает и превращает platform type в честный nullable/non-null; - у Spring и популярных библиотек такие аннотации уже расставлены — их API приходит в Kotlin с правильной nullability.
Правило: чем меньше platform types гуляет по коду, тем больше пользы от null-safety. На границе с Java — фиксируйте типы явно.
Коротко
- Возможность null — часть типа:
Stringне бывает null,String?— требует обработки перед использованием. - Инструменты:
?.— безопасный вызов цепочкой,?:— значение по умолчанию или ранний выход,let— действие над не-null,!!— аварийный люк с NPE. - После проверки
!= nullработает smart cast — явные касты не нужны. - Значения из Java приходят как platform types — компилятор их не проверяет; фиксируйте тип явно или размечайте Java-код аннотациями nullability.
- NPE в Kotlin-коде остаётся возможным ровно в двух местах:
!!и граница с Java.
Что почитать дальше
- Классы: data, sealed, object — следующий шаг маршрута.
- Kotlin и Spring Boot — nullability в контроллерах и DTO.
- Валидация запросов — null-safety не отменяет валидацию входных данных.