9.0 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Null-безопасность | Разделение типов на nullable и non-null, как компилятор предотвращает NPE |
|
Главная особенность системы типов Kotlin: возможность быть null - часть типа.
var a: String = "text"
// a = null // ошибка компиляции
var b: String? = "text"
b = null // ok
String и String? - разные типы. String является подтипом String?, но не наоборот.
graph TD
SN["String? — допускает null"]
S["String — не допускает null"]
N["Nothing? — только null"]
SN --> S
SN --> N
Зачем это нужно
NullPointerException в Java возникает в рантайме, иногда далеко от места, где null попал в переменную. В Kotlin компилятор отказывается собирать такой код:
fun length(s: String?): Int {
// return s.length // ошибка: only safe (?.) or non-null (!!) calls are allowed
return s?.length ?: 0
}
Проблема переносится с этапа выполнения на этап компиляции. Разыменовать nullable-значение без проверки невозможно.
Работа с nullable
Пять подходов, от предпочтительного к крайнему:
1. Проверка на null
После проверки компилятор сам приводит тип:
fun describe(s: String?): String {
if (s == null) return "нет значения"
return "длина ${s.length}" // здесь s уже String, не String?
}
Это умное приведение.
2. Безопасный вызов - ?.
val len: Int? = s?.length // null, если s == null
3. Elvis - ?:
val len: Int = s?.length ?: 0 // значение по умолчанию
val name = user?.name ?: error("нет пользователя") // или выход
4. Безопасное приведение - as?
val n: Int? = value as? Int // null вместо ClassCastException
5. Утверждение - !!
val len: Int = s!!.length // NPE, если s == null
!! означает «я гарантирую, что здесь не null». Каждое его использование - это место, где вы отключили проверку компилятора.
Подробнее про операторы: Операторы null-безопасности
Nullable в коллекциях
? ставится раздельно для элементов и для самой коллекции:
val a: List<String> = listOf("x") // список есть, элементы не null
val b: List<String?> = listOf(null) // список есть, элементы могут быть null
val c: List<String>? = null // списка может не быть
val d: List<String?>? = null // и то, и другое
Для отсеивания null есть готовые функции:
val items: List<String?> = listOf("a", null, "b")
println(items.filterNotNull()) // [a, b]
Nullable и дженерики
Параметр типа по умолчанию допускает null, его верхняя граница - Any?:
fun <T> process(item: T) { } // T может быть nullable
fun <T : Any> strict(item: T) { } // T гарантированно non-null
strict("text")
// strict(null) // ошибка компиляции
Подробнее: Параметрический полиморфизм
Где NPE всё-таки возможен
Kotlin не даёт абсолютной гарантии. Источники NPE:
- Явный
!!- вы сами попросили - Явный
throw NullPointerException() - Код на Java - типы оттуда компилятор проверить не может (Платформенные типы)
lateinitдо инициализации - техническиUninitializedPropertyAccessException- Утечка
thisиз конструктора - обращение к ещё не инициализированному свойству
class Broken {
val a: String = compute() // вызывается до инициализации b
val b: String = "value"
fun compute(): String = b.uppercase() // NPE: b ещё null
}
Последний случай компилятор обычно замечает и выдаёт предупреждение, но через несколько уровней вызовов - может и пропустить.
Правило
Если у вас много !! - типы спроектированы неправильно. Nullable-тип должен появляться там, где отсутствие значения - реальная часть предметной области («у пользователя может не быть отчества»), а не там, где лень разбираться с инициализацией.
[!info] Отличия от Java
Java Kotlin любая ссылка может быть null null допускают только типы с ?NPE в рантайме ошибка компиляции Optional<T>как обёрткаT?на уровне типов, без аллокации@Nullable/@NotNull- подсказки для IDEчасть системы типов, проверяется компилятором Objects.requireNonNull(x)requireNotNull(x)или просто non-null тип
Optional<T>в Java - это объект-обёртка: аллокация, нет поддержки в полях, не композируется с дженериками.T?в Kotlin не создаёт обёртки: в байт-коде это обычная ссылка, а проверки расставляет компилятор.Публичные функции Kotlin получают проверки параметров автоматически - вызов из Java с null упадёт сразу на входе, с понятным сообщением, а не глубже по стеку:
fun greet(name: String) = "Hi, $name"GreetKt.greet(null); // NullPointerException: Parameter specified as non-null is null: // method GreetKt.greet, parameter nameВ сообщении указаны и метод, и имя параметра.
Сравнить: Приведение ссылочных типов в Java
[!warning] Подводные камни
!!- не решение, а отложенная проблема. Строкаuser!!.profile!!.name!!падает без указания, какое из трёх звеньев было null.Умное приведение не работает с
varиз другого потока и с открытыми свойствами:class Holder { var value: String? = null fun use() { if (value != null) { // println(value.length) // ошибка: smart cast невозможен println(value!!.length) // или скопировать в локальную val } } }Подробнее: Умные приведения
Платформенные типы из Java проверок не имеют:
val s: String = javaMethodThatReturnsNull() // скомпилируется, упадёт в рантайме
Boolean?нельзя использовать вifнапрямую:val flag: Boolean? = null // if (flag) { } // ошибка компиляции if (flag == true) { } // так if (flag ?: false) { } // или так
nullв строковом шаблоне превращается в текст:val x: String? = null println("значение: $x") // значение: null
Далее: Операторы null-безопасности