Files
obsidian/Kotlin/Взаимодействие с Java/Взаимодействие с Java.md
T

203 lines
17 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: Взаимодействие с 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)