17 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Взаимодействие с Java | Зачем нужен interop, что работает без усилий, где возникает трение и как устроен раздел |
|
Kotlin проектировался как язык, который должен без потерь жить в существующей Java-экосистеме. Это не побочная возможность, а требование: без доступа к Spring, Jackson, JDBC и остальной инфраструктуре JVM язык был бы бесполезен.
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-плагином |
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!:
fun main() {
// Java-метод вернул null, а Kotlin считал тип ненулевым
val value: String = System.getProperty("несуществующее.свойство")
?: "значение по умолчанию"
println(value) // значение по умолчанию
}
Без ?: здесь был бы NullPointerException в момент присваивания. Подробнее: Платформенные типы
2. Изменяемость коллекций. В Kotlin List и MutableList - разные типы, в Java это один java.util.List:
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 считает неизменяемым. Подробнее: Изменяемость
3. Возможности Kotlin, которых нет в Java. Значения по умолчанию, функции-расширения, suspend, value-классы, object - всё это компилируется в конструкции JVM, но из Java выглядит иначе, чем задумано. Для управления этим есть аннотации @JvmStatic, @JvmOverloads, @JvmName и другие. Подробнее: Kotlin из Java
Асимметрия направлений
Направления не равноценны - и это главное, что нужно понять про interop.
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 | платформенные типы, SAM, массивы, статика, синтетические свойства |
| Kotlin из Java | @JvmStatic, @JvmOverloads, @JvmName, @JvmField, @Throws |
| Kotlin в байт-коде | во что компилируются свойства, лямбды, data class, suspend |
Смешанный проект
Gradle-плагин Kotlin компилирует Java и 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-интерфейс. Подробнее: Структура проекта
Стратегия миграции
Типичный путь для существующего Java-проекта:
- Новый код на Kotlin, старый не трогать. Interop позволяет это без прослоек.
- Конвертировать по файлу, начиная с листовых классов (DTO, утилиты) - у них меньше зависимостей.
- Проверять результат автоконвертера. IDE конвертирует Java в Kotlin, но результат получается «Java, написанная на Kotlin»: платформенные типы вместо nullable,
!!вместо?., классы вместоdata class. - Публичный API, вызываемый из Java, аннотировать сразу - иначе придётся менять его потом.
- Тесты конвертировать последними или не конвертировать - они и так работают.
// типичный результат автоконвертера — работает, но это не идиоматичный 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может бытьnullStringиString?различаютсяListодин типListиMutableListintи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 их нет:
throws IOExceptionиз Java-метода не обязывает ни кtry/catch, ни к объявлению. Это упрощает Kotlin-код, но Java-код, вызывающий Kotlin-метод, не узнает о возможном исключении без@Throws. Подробнее: Система исключенийNull-безопасность Kotlin не распространяется на Java-код. Компилятор не может проверить то, чего нет в типах Java. Поэтому аннотации
@Nullable/@NotNullв Java-коде - не документация, а реальная информация для компилятора Kotlin: с ними платформенные типы становятся обычными nullable/non-null.
[!warning] Подводные камни Платформенные типы отключают проверки компилятора. Значение из Java имеет тип
String!, который компилятор позволяет использовать и какString, и какString?. Ошибка проявится какNullPointerExceptionв рантайме, в точке присваивания: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
Далее: Java из Kotlin