--- 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() 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 = listOf("a", "b") // передача в Java-метод, который может вызвать add() val asJava: java.util.List = immutable as java.util.List 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-код"] -->|"почти без трения:
только null-безопасность"| B["Kotlin-код"] B -->|"требует аннотаций:
@JvmStatic, @JvmOverloads,
@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` из 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` и `List` в рантайме неразличимы по обе стороны границы, поэтому проверка типа элементов коллекции из Java невозможна. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md) Далее: [Java из Kotlin](Java%20из%20Kotlin.md)