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 не отменяет валидацию входных данных.