Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
---
|
||||
title: Взаимодействие с Java
|
||||
description: Зачем нужен interop, что работает без усилий, где возникает трение и как устроен раздел
|
||||
tags:
|
||||
- kotlin
|
||||
- java-interop
|
||||
---
|
||||
|
||||
Kotlin проектировался как язык, который должен без потерь жить в существующей Java-экосистеме. Это не побочная возможность, а требование: без доступа к Spring, Jackson, JDBC и остальной инфраструктуре JVM язык был бы бесполезен.
|
||||
|
||||
```kotlin
|
||||
import java.time.LocalDate
|
||||
import java.util.UUID
|
||||
|
||||
fun main() {
|
||||
val id = UUID.randomUUID()
|
||||
val today = LocalDate.of(2026, 1, 15)
|
||||
|
||||
println(id.toString().length) // 36
|
||||
println(today.plusDays(30)) // 2026-02-14
|
||||
println(today.dayOfWeek) // THURSDAY
|
||||
}
|
||||
```
|
||||
Это обычный Java API, вызванный как родной Kotlin-код: никаких обёрток, конвертеров и объявлений внешних типов.
|
||||
|
||||
## Что происходит без усилий
|
||||
| Что | Как работает |
|
||||
|---|---|
|
||||
| вызов Java-метода из Kotlin | как обычный метод |
|
||||
| геттеры/сеттеры Java | как свойства Kotlin |
|
||||
| Java-коллекции | как `MutableList`, `MutableMap` и т.д. |
|
||||
| наследование от Java-класса | как от Kotlin-класса |
|
||||
| реализация Java-интерфейса | SAM-конверсия из лямбды |
|
||||
| Java-аннотации | применяются к Kotlin-коду |
|
||||
| смешанная компиляция в одном модуле | поддерживается Gradle-плагином |
|
||||
|
||||
```kotlin
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
fun main() {
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
executor.submit { println("Runnable из лямбды") } // SAM-конверсия
|
||||
executor.shutdown()
|
||||
|
||||
val list = java.util.ArrayList<String>()
|
||||
list.add("элемент") // как MutableList
|
||||
println(list.size) // 1
|
||||
|
||||
val builder = StringBuilder()
|
||||
builder.append("a").append("b")
|
||||
println(builder.length) // 2
|
||||
// Runnable из лямбды
|
||||
}
|
||||
```
|
||||
|
||||
## Где возникает трение
|
||||
Три системы типов Kotlin не имеют соответствия в Java, и именно на этих границах появляются сложности.
|
||||
|
||||
**1. Null-безопасность.** Java не различает `String` и `String?`, поэтому любое значение из Java - платформенный тип `String!`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
// Java-метод вернул null, а Kotlin считал тип ненулевым
|
||||
val value: String = System.getProperty("несуществующее.свойство")
|
||||
?: "значение по умолчанию"
|
||||
println(value) // значение по умолчанию
|
||||
}
|
||||
```
|
||||
Без `?:` здесь был бы `NullPointerException` в момент присваивания. Подробнее: [Платформенные типы](../Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md)
|
||||
|
||||
**2. Изменяемость коллекций.** В Kotlin `List` и `MutableList` - разные типы, в Java это один `java.util.List`:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val immutable: List<String> = listOf("a", "b")
|
||||
// передача в Java-метод, который может вызвать add()
|
||||
val asJava: java.util.List<String> = immutable as java.util.List<String>
|
||||
val error = runCatching { asJava.add("c") }.exceptionOrNull()
|
||||
println("неизменяемый список отверг изменение: ${error != null}") // неизменяемый список отверг изменение: true
|
||||
}
|
||||
```
|
||||
Java-код не видит разницы и может попытаться изменить список, который Kotlin считает неизменяемым. Подробнее: [Изменяемость](../Коллекции/Изменяемость.md)
|
||||
|
||||
**3. Возможности Kotlin, которых нет в Java.** Значения по умолчанию, функции-расширения, `suspend`, value-классы, `object` - всё это компилируется в конструкции JVM, но из Java выглядит иначе, чем задумано. Для управления этим есть аннотации `@JvmStatic`, `@JvmOverloads`, `@JvmName` и другие. Подробнее: [Kotlin из Java](Kotlin%20из%20Java.md)
|
||||
|
||||
## Асимметрия направлений
|
||||
Направления не равноценны - и это главное, что нужно понять про interop.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["Java-код"] -->|"почти без трения:<br/>только null-безопасность"| B["Kotlin-код"]
|
||||
B -->|"требует аннотаций:<br/>@JvmStatic, @JvmOverloads,<br/>@JvmName, @JvmField"| A
|
||||
```
|
||||
|
||||
**Java из Kotlin** работает почти прозрачно: язык создавался именно для этого. Единственная серьёзная проблема - платформенные типы, то есть риск `NullPointerException` там, где компилятор уверен в ненулевом значении.
|
||||
|
||||
**Kotlin из Java** требует внимания: без аннотаций ваш API окажется неудобным - значения по умолчанию превратятся в один метод с лишними параметрами, `companion object` потребует обращения через `.Companion`, а `suspend`-функция получит параметр `Continuation`.
|
||||
|
||||
Практический вывод: если ваш Kotlin-код вызывается из Java, его публичный API нужно проектировать осознанно. Если Kotlin-код вызывается только из Kotlin, аннотации не нужны вообще.
|
||||
|
||||
## Карта раздела
|
||||
| Заметка | О чём |
|
||||
|---|---|
|
||||
| [Java из Kotlin](Java%20из%20Kotlin.md) | платформенные типы, SAM, массивы, статика, синтетические свойства |
|
||||
| [Kotlin из Java](Kotlin%20из%20Java.md) | `@JvmStatic`, `@JvmOverloads`, `@JvmName`, `@JvmField`, `@Throws` |
|
||||
| [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md) | во что компилируются свойства, лямбды, `data class`, `suspend` |
|
||||
|
||||
## Смешанный проект
|
||||
Gradle-плагин Kotlin компилирует Java и Kotlin вместе, разрешая циклические зависимости между языками:
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins {
|
||||
kotlin("jvm") version "2.4.0"
|
||||
java
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
}
|
||||
|
||||
// Kotlin и Java в одном модуле:
|
||||
// src/main/kotlin/ и src/main/java/ — компилируются совместно
|
||||
```
|
||||
Порядок компиляции: сначала Kotlin видит Java-исходники (через стабы), затем `javac` компилирует Java, уже видя готовые Kotlin-классы. Поэтому Java-класс может наследоваться от Kotlin-класса, который реализует Java-интерфейс. Подробнее: [Структура проекта](../Инструментарий/Структура%20проекта.md)
|
||||
|
||||
## Стратегия миграции
|
||||
Типичный путь для существующего Java-проекта:
|
||||
|
||||
1. **Новый код на Kotlin, старый не трогать.** Interop позволяет это без прослоек.
|
||||
2. **Конвертировать по файлу**, начиная с листовых классов (DTO, утилиты) - у них меньше зависимостей.
|
||||
3. **Проверять результат автоконвертера.** IDE конвертирует Java в Kotlin, но результат получается «Java, написанная на Kotlin»: платформенные типы вместо nullable, `!!` вместо `?.`, классы вместо `data class`.
|
||||
4. **Публичный API, вызываемый из Java, аннотировать** сразу - иначе придётся менять его потом.
|
||||
5. **Тесты конвертировать последними или не конвертировать** - они и так работают.
|
||||
|
||||
```kotlin
|
||||
// типичный результат автоконвертера — работает, но это не идиоматичный Kotlin
|
||||
class UserDtoConverted {
|
||||
var name: String? = null
|
||||
var age: Int? = null
|
||||
fun getDisplayName(): String {
|
||||
return if (name != null) name!! else "аноним"
|
||||
}
|
||||
}
|
||||
|
||||
// идиоматичная версия
|
||||
data class UserDto(val name: String?, val age: Int?) {
|
||||
val displayName: String get() = name ?: "аноним"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(UserDtoConverted().getDisplayName()) // аноним
|
||||
println(UserDto(null, null).displayName) // аноним
|
||||
}
|
||||
```
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | один язык, одна система типов | две системы, граница между ними |
|
||||
> | `String` может быть `null` | `String` и `String?` различаются |
|
||||
> | `List` один тип | `List` и `MutableList` |
|
||||
> | `int` и `Integer` различаются | `Int` и `Int?` |
|
||||
> | статические методы | `companion object` + `@JvmStatic` |
|
||||
> | нет аналога | платформенные типы `String!` |
|
||||
> | checked exceptions обязательны | проверяемых исключений нет |
|
||||
>
|
||||
> **Interop работает, потому что Kotlin компилируется в тот же байт-код.** Kotlin-класс - это обычный класс JVM: у него те же модификаторы доступа, то же наследование, тот же формат `.class`. Никакой прослойки, маршалинга или рефлексии в runtime нет, поэтому вызов Kotlin-метода из Java стоит ровно столько же, сколько вызов Java-метода. Это принципиально отличает ситуацию от interop между языками разных платформ (JNI, JNA), где границу приходится пересекать с накладными расходами.
|
||||
>
|
||||
> **Цена - необходимость представлять конструкции Kotlin средствами JVM.** В байт-коде нет ни nullable-типов, ни значений по умолчанию, ни функций-расширений. Kotlin эмулирует их: nullable-информацию хранит в аннотациях и метаданных, значения по умолчанию - в синтетическом методе `greet$default`, расширения - в статических методах. Из Kotlin это видно как родные конструкции (компилятор читает метаданные), из Java - как реализация. Подробнее: [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md)
|
||||
>
|
||||
> **Проверяемые исключения - односторонняя потеря.** В Kotlin их нет: `throws IOException` из Java-метода не обязывает ни к `try/catch`, ни к объявлению. Это упрощает Kotlin-код, но Java-код, вызывающий Kotlin-метод, не узнает о возможном исключении без `@Throws`. Подробнее: [Система исключений](../Исключения/Система%20исключений.md)
|
||||
>
|
||||
> **Null-безопасность Kotlin не распространяется на Java-код.** Компилятор не может проверить то, чего нет в типах Java. Поэтому аннотации `@Nullable`/`@NotNull` в Java-коде - не документация, а реальная информация для компилятора Kotlin: с ними платформенные типы становятся обычными nullable/non-null.
|
||||
>
|
||||
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md), [Байт-код](../../Программирование%20на%20Java/JVM/Байт-код.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Платформенные типы отключают проверки компилятора.** Значение из Java имеет тип `String!`, который компилятор позволяет использовать и как `String`, и как `String?`. Ошибка проявится как `NullPointerException` в рантайме, в точке присваивания:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val error = runCatching {
|
||||
> val value: String = System.getProperty("нет.такого") // вернёт null
|
||||
> value.length
|
||||
> }.exceptionOrNull()
|
||||
> println("NPE от платформенного типа: ${error is NullPointerException}")
|
||||
> // NPE от платформенного типа: true
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Автоконвертер даёт работающий, но плохой код.** Типичные следы: `!!` вместо `?.`, `var x: T? = null` вместо конструктора, геттеры вместо свойств, обычные классы вместо `data class`. После конвертации файл нужно переписывать вручную, иначе Kotlin-проект унаследует все недостатки Java-кода и не получит преимуществ.
|
||||
>
|
||||
> **Java-код может нарушить неизменяемость Kotlin-коллекции.** `List<String>` из Kotlin приходит в Java как `java.util.List`, у которого есть `add()`. Вызов упадёт с `UnsupportedOperationException` в рантайме, а компилятор об этом не предупредит. Передавайте в Java копию (`toList()`) или `Collections.unmodifiableList`.
|
||||
>
|
||||
> **Kotlin-код без аннотаций неудобен из Java.** Значения по умолчанию, `companion object`, `object`, `suspend`-функции и функции верхнего уровня выглядят из Java не так, как задумано. Если API публичный, это нужно решать до релиза - добавление `@JvmStatic` позже меняет сигнатуры и ломает совместимость.
|
||||
>
|
||||
> **`internal` в Kotlin из Java доступен.** Он компилируется в `public` с манглированным именем (`internalFun$main`), поэтому Java-код в том же проекте технически может его вызвать. Это не защита, а соглашение.
|
||||
>
|
||||
> **Проверяемые исключения из Kotlin не видны Java.** Kotlin-метод, бросающий `IOException`, из Java выглядит как не бросающий ничего - `catch (IOException e)` вокруг него не скомпилируется («exception is never thrown»). Нужна аннотация `@Throws`.
|
||||
>
|
||||
> **Смешанная компиляция требует правильного плагина.** В Gradle нужен `kotlin("jvm")` и, при наличии Java-исходников, корректно настроенный `sourceSets`. Без этого Java-код не увидит Kotlin-классы, а ошибка выглядит как «cannot find symbol».
|
||||
>
|
||||
> **Generics Java стираются и в Kotlin.** `List<String>` и `List<Int>` в рантайме неразличимы по обе стороны границы, поэтому проверка типа элементов коллекции из Java невозможна. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md)
|
||||
|
||||
Далее: [Java из Kotlin](Java%20из%20Kotlin.md)
|
||||
Reference in New Issue
Block a user