161 lines
8.6 KiB
Markdown
161 lines
8.6 KiB
Markdown
---
|
||
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)
|