203 lines
17 KiB
Markdown
203 lines
17 KiB
Markdown
---
|
||
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)
|