Added kotlin guide. (WIP) (15.10.26)
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user