--- title: Платформенные типы description: Тип T! из Java-кода, где null-безопасность Kotlin перестаёт работать и как это чинить tags: - kotlin - null-safety - interop --- Платформенный тип (platform type) - тип, пришедший из Java, про nullability которого компилятор Kotlin ничего не знает. Обозначается `T!`, но написать его в коде нельзя - он существует только в сообщениях компилятора и в IDE. ```java // Java public class Repo { public String findName() { return null; } } ``` ```kotlin // 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 возникает в точке присваивания, а не использования - компилятор вставляет проверку на границе: ```kotlin val a: String = repo.findName() // NPE здесь же println(a.length) // до этой строки не дойдёт ``` Это лучше, чем в Java: падение происходит рядом с источником, а не там, куда значение утекло. ## Как чинить ### 1. Указывать nullable-тип явно Любое значение из Java трактуйте как `T?`, пока не убедились в обратном: ```kotlin val name: String? = repo.findName() println(name?.length ?: 0) ``` ### 2. Аннотации в Java-коде Компилятор Kotlin читает аннотации nullability и начинает проверять типы: ```java 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"; } } ``` ```kotlin 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: ```kotlin // вся работа с «грязными» типами собрана здесь fun Repo.nameOrNull(): String? = findName() fun Repo.requireName(): String = findName() ?: error("name отсутствует") ``` Дальше код работает с честными типами Kotlin. ### 4. Строгий режим компилятора Флаг `-Xjsr305=strict` заставляет компилятор относиться к JSR-305-аннотациям как к обязательным: ```kotlin // build.gradle.kts kotlin { compilerOptions { freeCompilerArgs.add("-Xjsr305=strict") } } ``` Значения: `ignore`, `warn` (по умолчанию), `strict`. ## Коллекции из Java Платформенность распространяется на параметры типа: ```java public List getItems() { return null; } ``` Тип результата - `(Mutable)List!`. Неизвестно ни то, может ли быть null сам список, ни его элементы: ```kotlin val items: List? = repo.items // самое честное предположение items?.filterNotNull()?.forEach(::println) ``` Отдельная проблема - изменяемость. `java.util.List` отображается в `(Mutable)List`, то есть Kotlin позволит передать его туда, где ожидается `MutableList`, и Java-код может изменить «неизменяемый» список: ```kotlin val list: List = repo.items // на самом деле ArrayList // list.add("x") // так нельзя, тип List (list as MutableList).add("x") // а так — да, и это сработает ``` Подробнее: [Java из Kotlin](../../Взаимодействие%20с%20Java/Java%20из%20Kotlin.md) ## Обратное направление Код на Kotlin для Java-вызывающего выглядит аннотированным: компилятор добавляет `@NotNull`/`@Nullable` в байт-код автоматически. Плюс в публичные функции вставляются проверки параметров: ```kotlin fun greet(name: String) = "Hi, $name" ``` ```java GreetKt.greet(null); // NullPointerException: Parameter specified as non-null is null: // method GreetKt.greet, parameter name ``` Падение происходит на входе в функцию, с указанием метода и имени параметра. > [!info] Отличия от Java > В Java платформенных типов нет - там все ссылки одинаково могут быть null, и никакой проверки не существует. > > Платформенные типы - это механизм совместимости, а не фича языка. Их задача - позволить Kotlin вызывать существующие библиотеки без переписывания. Чем больше кода в проекте на Kotlin, тем реже они встречаются. > > Сравнить: [Система исключений в Java](../../../Программирование%20на%20Java/Исключения/Система%20исключений.md) > [!warning] Подводные камни > **Самый опасный случай - значение «просачивается» в non-null тип:** > ```kotlin > 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 особенно коварны:** > ```java > public T getOrNull(String key) { return null; } > ``` > ```kotlin > val x: String = repo.getOrNull("k") // T выведется в String, NPE в рантайме > ``` > > **Массивы из Java тоже платформенные:** `String[]` → `Array<(out) String!>!`. Элементы могут быть null даже если сам массив не null. Дополнительно: [Java из Kotlin](../../Взаимодействие%20с%20Java/Java%20из%20Kotlin.md) Далее: [Строки](../Работа%20с%20данными/Строки.md)