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