Files
obsidian/Kotlin/Синтаксис и типы/Null-безопасность/Платформенные типы.md
T

8.6 KiB
Raw Blame History

title, description, tags
title description tags
Платформенные типы Тип T! из Java-кода, где null-безопасность Kotlin перестаёт работать и как это чинить
kotlin
null-safety
interop

Платформенный тип (platform type) - тип, пришедший из Java, про nullability которого компилятор Kotlin ничего не знает. Обозначается T!, но написать его в коде нельзя - он существует только в сообщениях компилятора и в IDE.

// Java
public class Repo {
    public String findName() { return null; }
}
// Kotlin
val repo = Repo()
val a: String = repo.findName()    // компилируется! падает в рантайме
val b: String? = repo.findName()   // тоже компилируется

findName() имеет тип String! - его можно присвоить и в String, и в String?. Проверку компилятор не делает.

Почему так

Альтернативы были хуже:

  • считать все Java-типы nullable → весь Java-код пришлось бы обвешивать !! и ?., включая System.out
  • считать все non-null → ложное чувство безопасности без возможности это заметить

Платформенный тип - компромисс: ответственность за nullability возвращается программисту, как в Java.

Где падает

NPE возникает в точке присваивания, а не использования - компилятор вставляет проверку на границе:

val a: String = repo.findName()    // NPE здесь же
println(a.length)                  // до этой строки не дойдёт

Это лучше, чем в Java: падение происходит рядом с источником, а не там, куда значение утекло.

Как чинить

1. Указывать nullable-тип явно

Любое значение из Java трактуйте как T?, пока не убедились в обратном:

val name: String? = repo.findName()
println(name?.length ?: 0)

2. Аннотации в Java-коде

Компилятор Kotlin читает аннотации nullability и начинает проверять типы:

import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.NotNull;

public class Repo {
    @Nullable public String findName() { return null; }
    @NotNull  public String getId()    { return "1"; }
}
val a: String = repo.findName()   // теперь ошибка компиляции
val b: String? = repo.findName()  // ok
val c: String = repo.getId()      // ok, проверка не нужна

Поддерживаются аннотации из нескольких наборов:

Набор Пакет
JetBrains org.jetbrains.annotations
JSpecify org.jspecify.annotations
Android androidx.annotation
JSR-305 javax.annotation
Eclipse org.eclipse.jdt.annotation
Lombok lombok.NonNull

3. Обёртка на границе

Для внешней библиотеки без аннотаций - свой слой, который один раз разбирается с nullability:

// вся работа с «грязными» типами собрана здесь
fun Repo.nameOrNull(): String? = findName()
fun Repo.requireName(): String = findName() ?: error("name отсутствует")

Дальше код работает с честными типами Kotlin.

4. Строгий режим компилятора

Флаг -Xjsr305=strict заставляет компилятор относиться к JSR-305-аннотациям как к обязательным:

// build.gradle.kts
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xjsr305=strict")
    }
}

Значения: ignore, warn (по умолчанию), strict.

Коллекции из Java

Платформенность распространяется на параметры типа:

public List<String> getItems() { return null; }

Тип результата - (Mutable)List<String!>!. Неизвестно ни то, может ли быть null сам список, ни его элементы:

val items: List<String?>? = repo.items   // самое честное предположение
items?.filterNotNull()?.forEach(::println)

Отдельная проблема - изменяемость. java.util.List отображается в (Mutable)List, то есть Kotlin позволит передать его туда, где ожидается MutableList, и Java-код может изменить «неизменяемый» список:

val list: List<String> = repo.items   // на самом деле ArrayList
// list.add("x")                      // так нельзя, тип List
(list as MutableList).add("x")        // а так — да, и это сработает

Подробнее: Java из Kotlin

Обратное направление

Код на Kotlin для Java-вызывающего выглядит аннотированным: компилятор добавляет @NotNull/@Nullable в байт-код автоматически. Плюс в публичные функции вставляются проверки параметров:

fun greet(name: String) = "Hi, $name"
GreetKt.greet(null);
// NullPointerException: Parameter specified as non-null is null:
// method GreetKt.greet, parameter name

Падение происходит на входе в функцию, с указанием метода и имени параметра.

[!info] Отличия от Java В Java платформенных типов нет - там все ссылки одинаково могут быть null, и никакой проверки не существует.

Платформенные типы - это механизм совместимости, а не фича языка. Их задача - позволить Kotlin вызывать существующие библиотеки без переписывания. Чем больше кода в проекте на Kotlin, тем реже они встречаются.

Сравнить: Система исключений в Java

[!warning] Подводные камни Самый опасный случай - значение «просачивается» в non-null тип:

data class User(val name: String)
val user = User(repo.findName())   // NPE в конструкторе, а причина — в Java-методе

IDE подсказывает тип String!, но в коде так написать нельзя. Если увидели ! в подсказке - значит, компилятор вас не защищает.

Аннотации на уровне пакета легко пропустить. @NonNullApi или package-info.java с @ParametersAreNonnullByDefault меняют поведение для всего пакета. Spring, например, аннотирует свои пакеты - поэтому его API в Kotlin выглядит типизированным.

-Xjsr305=strict может сломать сборку при обновлении библиотеки - новые аннотации превратятся в ошибки компиляции. Включать осознанно.

Дженерик-методы Java особенно коварны:

public <T> T getOrNull(String key) { return null; }
val x: String = repo.getOrNull("k")   // T выведется в String, NPE в рантайме

Массивы из Java тоже платформенные: String[] → Array<(out) String!>!. Элементы могут быть null даже если сам массив не null.

Дополнительно: Java из Kotlin Далее: Строки