8.6 KiB
title, description, tags
| title | description | tags | |||
|---|---|---|---|---|---|
| Платформенные типы | Тип T! из Java-кода, где null-безопасность Kotlin перестаёт работать и как это чинить |
|
Платформенный тип (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 Далее: Строки