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

161 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<String> getItems() { return null; }
```
Тип результата - `(Mutable)List<String!>!`. Неизвестно ни то, может ли быть null сам список, ни его элементы:
```kotlin
val items: List<String?>? = repo.items // самое честное предположение
items?.filterNotNull()?.forEach(::println)
```
Отдельная проблема - изменяемость. `java.util.List` отображается в `(Mutable)List`, то есть Kotlin позволит передать его туда, где ожидается `MutableList`, и Java-код может изменить «неизменяемый» список:
```kotlin
val list: List<String> = 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> 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)