Вызов Kotlin из Java
Код Kotlin можно легко вызывать из Java. Например, экземпляры класса Kotlin можно без проблем создавать и использовать в методах Java. Однако между Java и Kotlin есть определённые различия, которые необходимо учитывать при интеграции кода Kotlin в Java. На этой странице мы расскажем, как настроить взаимодействие кода Kotlin с клиентами на Java.
Свойства
Свойство Kotlin компилируется в следующие элементы Java:
метод-геттер, имя которого вычисляется добавлением префикса
get.метод-сеттер, имя которого вычисляется добавлением префикса
set(только для свойствvar).закрытое поле с тем же именем, что и у свойства (только для свойств с резервными полями).
Например, var firstName: String компилируется в следующие объявления Java:
private String firstName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
Если имя свойства начинается с is, используется другое правило сопоставления имён: имя геттера совпадает с именем свойства, а имя сеттера получается заменой is на set. Например, для свойства isOpen геттер называется isOpen(), а сеттер — setOpen(). Это правило применяется к свойствам любого типа, а не только Boolean.
Функции уровня пакета
Все функции и свойства, объявленные в файле app.kt внутри пакета org.example, включая функции расширения, компилируются в статические методы Java-класса с именем org.example.AppKt.
// app.kt
package org.example
class Util
fun getTime() { /*...*/ }
// Java new org.example.Util(); org.example.AppKt.getTime();
Чтобы задать сгенерированному Java-классу пользовательское имя, используйте аннотацию @JvmName:
@file:JvmName("DemoUtils")
package org.example
class Util
fun getTime() { /*...*/ }
// Java new org.example.Util(); org.example.DemoUtils.getTime();
Наличие нескольких файлов с одинаковым именем сгенерированного Java-класса (одинаковые пакет и имя либо одинаковая аннотация @JvmName) обычно считается ошибкой. Однако компилятор может сгенерировать один класс-фасад Java с указанным именем, содержащий все объявления из всех файлов с этим именем. Чтобы включить генерацию такого фасада, используйте аннотацию @JvmMultifileClass во всех таких файлах.
// oldutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass
package org.example
fun getTime() { /*...*/ }
// newutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass
package org.example
fun getDate() { /*...*/ }
// Java org.example.Utils.getTime(); org.example.Utils.getDate();
Поля экземпляра
Если нужно представить свойство Kotlin в Java как поле, снабдите его аннотацией @JvmField. Поле имеет ту же видимость, что и исходное свойство. Свойство можно снабдить аннотацией @JvmField, если оно:
имеет резервное поле
не является закрытым
не имеет модификаторов
open,overrideилиconstне является делегированным свойством
class User(id: String) {
@JvmField val ID = id
}
// Java
class JavaClient {
public String getID(User user) {
return user.ID;
}
}
Свойства с отложенной инициализацией также представляются в виде полей. Видимость поля совпадает с видимостью сеттера свойства lateinit.
Статические поля
У свойств Kotlin, объявленных в именованном объекте или объекте-компаньоне, есть статические резервные поля — либо в этом именованном объекте, либо в классе, содержащем объект-компаньон.
Обычно эти поля закрытые, но их можно сделать доступными одним из следующих способов:
аннотация
@JvmFieldмодификатор
lateinitмодификатор
const
Аннотация свойства @JvmField превращает его в статическое поле с той же видимостью, что и у самого свойства.
class Key(val value: Int) {
companion object {
@JvmField
val COMPARATOR: Comparator<Key> = compareBy<Key> { it.value }
}
}
// Java Key.COMPARATOR.compare(key1, key2); // public static final field in Key class
Свойство с отложенной инициализацией в объекте или объекте-компаньоне имеет статическое резервное поле с той же видимостью, что и у сеттера свойства.
object Singleton {
lateinit var provider: Provider
}
// Java Singleton.provider = new Provider(); // public static non-final field in Singleton class
Свойства, объявленные как const (как в классах, так и на верхнем уровне), преобразуются в статические поля в Java:
// file example.kt
object Obj {
const val CONST = 1
}
class C {
companion object {
const val VERSION = 9
}
}
const val MAX = 239
В Java:
int constant = Obj.CONST; int max = ExampleKt.MAX; int version = C.VERSION;
Статические методы
Kotlin представляет функции уровня пакета как статические методы. Статические методы также можно сгенерировать для функций, определённых в именованных объектах или объектах-компаньонах, снабдив такие функции аннотацией @JvmStatic.
Если использовать @JvmStatic для функции в объекте-компаньоне, компилятор создаст и статический метод во внешнем классе, и метод экземпляра в объекте-компаньоне:
// Kotlin
class C {
companion object {
@JvmStatic fun callStatic() {}
fun callNonStatic() {}
}
}
В Java можно вызывать callStatic() как у внешнего класса, так и у объекта-компаньона, тогда как callNonStatic() доступен только через объект-компаньон:
// Java C.callStatic(); // Success C.callNonStatic(); // Error: not a static method C.Companion.callStatic(); // Instance method remains C.Companion.callNonStatic(); // Success
Для именованного объекта (синглтона) @JvmStatic превращает функцию в статический метод класса объекта, но не создаёт отдельный метод экземпляра:
// Kotlin
object Obj {
@JvmStatic fun callStatic() {}
fun callNonStatic() {}
}
В Java метод callStatic() можно вызвать у именованного объекта, тогда как callNonStatic() доступен только через экземпляр синглтона:
// Java Obj.callStatic(); // Success Obj.callNonStatic(); // Error: not a static method Obj.INSTANCE.callNonStatic(); // Success, the call passed through the singleton instance
Также можно снабдить аннотацией @JvmStatic функцию в объекте-компаньоне интерфейса. Такие функции компилируются в статические методы интерфейсов:
interface ChatBot {
companion object {
@JvmStatic fun greet(username: String) {
println("Hello, $username")
}
}
}
Аннотацию @JvmStatic также можно применить к свойству объекта или объекта-компаньона, чтобы его методы-геттеры и сеттеры стали статическими членами этого объекта или класса, содержащего объект-компаньон.
Методы по умолчанию в интерфейсах
При компиляции для JVM Kotlin преобразует функции, объявленные в интерфейсах, в методы по умолчанию, если не настроено иное. Это конкретные методы в интерфейсах, которые классы Java могут наследовать напрямую, без повторной реализации.
Ниже приведён пример интерфейса Kotlin с методом по умолчанию:
interface Robot {
fun move() { println("~walking~") } // will be default in the Java interface
fun speak(): Unit
}
Реализация по умолчанию доступна классам Java, реализующим интерфейс.
//Java implementation
public class C3PO implements Robot {
// move() implementation from Robot is available implicitly
@Override
public void speak() {
System.out.println("I beg your pardon, sir");
}
}
C3PO c3po = new C3PO(); c3po.move(); // default implementation from the Robot interface c3po.speak();
Реализации интерфейса могут переопределять методы по умолчанию.
//Java
public class BB8 implements Robot {
//own implementation of the default method
@Override
public void move() {
System.out.println("~rolling~");
}
@Override
public void speak() {
System.out.println("Beep-beep");
}
}
Режимы совместимости для методов по умолчанию
Kotlin предоставляет три режима управления компиляцией функций интерфейсов в методы JVM по умолчанию. Эти режимы определяют, будет ли компилятор генерировать мосты совместимости и статические методы в классах DefaultImpls.
Управлять этим поведением можно с помощью параметра компилятора -jvm-default:
Подробнее о режимах совместимости:
enable
Поведение по умолчанию. Генерирует реализации по умолчанию в интерфейсах, а также мосты совместимости и классы DefaultImpls. Этот режим обеспечивает совместимость с ранее скомпилированным кодом Kotlin.
no-compatibility
Генерирует только реализации по умолчанию в интерфейсах. Пропускает мосты совместимости и классы DefaultImpls. Используйте этот режим для новых кодовых баз, которые не взаимодействуют с кодом, использующим классы DefaultImpls. Это может нарушить бинарную совместимость с более старым кодом Kotlin.
disable
Отключает реализации по умолчанию в интерфейсах. Генерируются только мосты совместимости и классы DefaultImpls.
Видимость
Видимость в Kotlin преобразуется в модификаторы Java следующим образом:
Члены
privateостаютсяprivate.Объявления верхнего уровня
privateстановятся объявлениями верхнего уровняprivateв Java. Также добавляются средства доступа с видимостью на уровне пакета, если к ним обращаются из класса.-
Члены
protectedостаютсяprotected.Обратите внимание: Java позволяет обращаться к защищённым членам из других классов того же пакета, а Kotlin — нет.
-
Объявления
internalстановятсяpublicв Java.Компилятор Kotlin изменяет имена членов
internalв байт-коде. Это предотвращает случайные переопределения между модулями, например при наследовании класса Kotlin в Java, и позволяет перегружать члены с одинаковой сигнатурой.Обратите внимание: имена открытых членов классов
internalне изменяются, и к ним по-прежнему можно обращаться из Java. Члены
publicостаютсяpublic.
KClass
Иногда необходимо вызвать метод Kotlin с параметром типа KClass. Автоматического преобразования Class в KClass нет, поэтому его нужно выполнить вручную, вызвав аналог свойства-расширения Class<T>.kotlin:
kotlin.jvm.JvmClassMappingKt.getKotlinClass(MainView.class)
Разрешение конфликтов сигнатур с помощью @JvmName
Иногда для именованной функции Kotlin требуется другое имя в байт-коде JVM. Самый известный пример связан со стиранием типов:
fun List<String>.filterValid(): List<String> fun List<Int>.filterValid(): List<Int>
Эти две функции нельзя объявить рядом, поскольку их сигнатуры JVM совпадают: filterValid(Ljava/util/List;)Ljava/util/List;. Если нам всё же нужно, чтобы в Kotlin у них было одинаковое имя, можно снабдить одну из них (или обе) аннотацией @JvmName и указать в качестве аргумента другое имя:
fun List<String>.filterValid(): List<String>
@JvmName("filterValidInt")
fun List<Int>.filterValid(): List<Int>
Из Kotlin к ним можно обращаться под одним и тем же именем filterValid, а из Java — под именами filterValid и filterValidInt.
Этот же приём применим, когда нужно иметь свойство x и функцию getX():
val x: Int
@JvmName("getX_prop")
get() = 15
fun getX() = 10
Чтобы изменить имена сгенерированных методов доступа для свойств без явно реализованных геттеров и сеттеров, можно использовать @get:JvmName и @set:JvmName:
@get:JvmName("x")
@set:JvmName("changeX")
var x: Int = 23
Генерация перегрузок
Обычно функция Kotlin со значениями параметров по умолчанию доступна в Java только с полной сигнатурой, то есть со всеми параметрами.
Чтобы генерировать перегрузки для необязательных параметров, можно использовать аннотацию @IntroducedAt или @JvmOverloads.
Используйте @IntroducedAt, если добавляете новые необязательные параметры в опубликованные API и хотите, чтобы сгенерированные перегрузки соответствовали версии, в которой был добавлен каждый параметр. На основе этой информации компилятор автоматически генерирует соответствующие скрытые перегрузки.
Это позволяет генерировать перегрузки с учётом версий и помогает сохранять бинарную совместимость для вызывающего кода, скомпилированного с более старыми версиями библиотеки.
Рассмотрим пример, в котором функция Button() получает несколько необязательных параметров в нескольких версиях API:
@OptIn(ExperimentalVersionOverloading::class)
fun Button(
label: String = "",
color: Color = DefaultColor,
@IntroducedAt("1.1") borderColor: Color = DefaultBorderColor,
@IntroducedAt("1.2") borderStyle: Style = DefaultBorderStyle,
@IntroducedAt("1.2") borderWidth: Int = 1,
onClick: () -> Unit
) {
// Function body
}
На основе этих версий компилятор генерирует скрытые перегрузки для исходного API и для каждой версии API, в которой появились новые необязательные параметры:
// Original API
Button(
label: String,
color: Color,
onClick: () -> Unit
)
// Version 1.1
Button(
label: String,
color: Color,
borderColor: Color,
onClick: () -> Unit
)
// Version 1.2
Button(
label: String,
color: Color,
borderColor: Color,
borderStyle: Style,
borderWidth: Int,
onClick: () -> Unit
)
Если вы хотите предоставить вызывающему коду Java несколько перегрузок, можно также использовать аннотацию @JvmOverloads.
Аннотация также работает с конструкторами, статическими методами и так далее. Её нельзя использовать для абстрактных методов, в том числе методов, определённых в интерфейсах. Например, рассмотрим класс Circle со значениями параметров по умолчанию:
class Circle @JvmOverloads constructor(centerX: Int, centerY: Int, radius: Double = 1.0) {
@JvmOverloads fun draw(label: String, lineWidth: Int = 1, color: String = "red") { /*...*/ }
}
Для каждого параметра со значением по умолчанию генерируется одна дополнительная перегрузка, из списка параметров которой удаляются этот параметр и все параметры справа от него. В этом примере будет сгенерировано следующее:
// Constructors:
Circle(int centerX, int centerY, double radius)
Circle(int centerX, int centerY)
// Methods
void draw(String label, int lineWidth, String color) { }
void draw(String label, int lineWidth) { }
void draw(String label) { }
Поскольку обе аннотации — @IntroducedAt и @JvmOverloads — генерируют перегрузки, их совместное использование может привести к конфликтующим перегрузкам. При использовании обеих аннотаций компилятор выдаёт предупреждение. Если отключить это предупреждение, компилятор отдаёт приоритет перегрузкам, сгенерированным на основе аннотации @IntroducedAt.
Как описано в разделе Вторичные конструкторы, если для всех параметров конструктора класса заданы значения по умолчанию, для него генерируется открытый конструктор без аргументов. Это работает и без указания аннотации @JvmOverloads.
Проверяемые исключения
В Kotlin нет проверяемых исключений. Поэтому в сигнатурах функций Kotlin для Java обычно не указываются выбрасываемые исключения. Таким образом, если в Kotlin есть функция следующего вида:
// example.kt
package demo
fun writeToFile() {
/*...*/
throw IOException()
}
И вы хотите вызвать её из Java и перехватить исключение:
// Java
try {
demo.Example.writeToFile();
} catch (IOException e) {
// error: writeToFile() does not declare IOException in the throws list
// ...
}
Компилятор Java выдаст сообщение об ошибке, поскольку writeToFile() не объявляет IOException. Чтобы обойти эту проблему, используйте в Kotlin аннотацию @Throws:
@Throws(IOException::class)
fun writeToFile() {
/*...*/
throw IOException()
}
Безопасность относительно null
При вызове функций Kotlin из Java ничто не мешает передать null вместо параметра, не допускающего null. Поэтому Kotlin генерирует проверки во время выполнения для всех открытых функций, ожидающих ненулевые значения. Благодаря этому ошибка NullPointerException сразу возникает в коде Java.
Вариантные обобщённые типы
Если в классах Kotlin используется вариантность на месте объявления, то в коде Java их можно использовать двумя способами. Например, представьте, что у вас есть следующий класс и две функции, которые его используют:
class Box<out T>(val value: T) interface Base class Derived : Base fun boxDerived(value: Derived): Box<Derived> = Box(value) fun unboxBase(box: Box<Base>): Base = box.value
Наивный способ перевести эти функции на Java выглядел бы так:
Box<Derived> boxDerived(Derived value) { ... }
Base unboxBase(Box<Base> box) { ... }
Проблема в том, что в Kotlin можно написать unboxBase(boxDerived(Derived())), а в Java это невозможно, поскольку класс Box в Java инвариантен относительно параметра T, поэтому Box<Derived> не является подтипом Box<Base>. Чтобы это работало в Java, объявление unboxBase пришлось бы записать так:
Base unboxBase(Box<? extends Base> box) { ... }
В этом объявлении используются подстановочные типы Java (? extends Base), чтобы имитировать вариантность на месте объявления с помощью вариантности на месте использования, поскольку в Java доступен только такой способ.
Чтобы API Kotlin работали в Java, компилятор генерирует Box<Super> как Box<? extends Super> для ковариантно объявленных Box (или Foo<? super Bar> для контравариантно объявленных Foo), когда они используются в качестве параметра. Для возвращаемого значения подстановочные типы не генерируются, иначе клиентскому коду Java пришлось бы с ними работать (что противоречит общепринятому стилю кода Java). Таким образом, функции из нашего примера фактически преобразуются следующим образом:
// return type - no wildcards
Box<Derived> boxDerived(Derived value) { ... }
// parameter - wildcards
Base unboxBase(Box<? extends Base> box) { ... }
Если подстановочные типы нужны там, где они не генерируются по умолчанию, используйте аннотацию @JvmWildcard:
fun boxDerived(value: Derived): Box<@JvmWildcard Derived> = Box(value)
// is translated to
// Box<? extends Derived> boxDerived(Derived value) { ... }
И наоборот, если подстановочные типы не нужны там, где они генерируются, используйте @JvmSuppressWildcards:
fun unboxBase(box: Box<@JvmSuppressWildcards Base>): Base = box.value
// is translated to
// Base unboxBase(Box<Base> box) { ... }
Преобразование типа Nothing
Тип Nothing является особым, поскольку в Java ему не соответствует естественный аналог. Действительно, любой ссылочный тип Java, включая java.lang.Void, принимает null в качестве значения, а Nothing не принимает даже его. Поэтому этот тип нельзя точно представить в мире Java. По этой причине Kotlin генерирует тип без параметров, если используется аргумент типа Nothing:
fun emptyList(): List<Nothing> = listOf()
// is translated to
// List emptyList() { ... }
Встраиваемые классы-значения
Чтобы код Java мог без проблем работать со встраиваемыми классами-значениями Kotlin, можно использовать аннотацию @JvmExposeBoxed или параметр компилятора -Xjvm-expose-boxed. Эти способы обеспечивают генерацию Kotlin необходимых упакованных представлений для взаимодействия с Java.
По умолчанию Kotlin компилирует встраиваемые классы-значения в распакованное представление, которое часто недоступно из Java. Например, из Java нельзя вызвать конструктор класса MyInt:
@JvmInline value class MyInt(val value: Int)
Поэтому следующий код Java завершится ошибкой:
MyInt input = new MyInt(5);
Можно использовать аннотацию @JvmExposeBoxed, чтобы Kotlin сгенерировал открытый конструктор, который можно вызывать напрямую из Java. Для точного управления тем, что будет доступно из Java, аннотацию можно применять на следующих уровнях:
Класс
Конструктор
Функция
Перед использованием аннотации @JvmExposeBoxed в коде необходимо явно разрешить её применение с помощью @OptIn(ExperimentalStdlibApi::class). Например:
@OptIn(ExperimentalStdlibApi::class) @JvmExposeBoxed @JvmInline value class MyInt(val value: Int) @OptIn(ExperimentalStdlibApi::class) @JvmExposeBoxed fun MyInt.timesTwoBoxed(): MyInt = MyInt(this.value * 2)
С этими аннотациями Kotlin генерирует доступный из Java конструктор для класса MyInt и вариант функции расширения, использующий упакованную форму класса-значения. Поэтому следующий код Java выполняется успешно:
MyInt input = new MyInt(5); MyInt output = ExampleKt.timesTwoBoxed(input);
Чтобы применить это поведение ко всем встраиваемым классам-значениям и использующим их функциям в модуле, скомпилируйте его с параметром -Xjvm-expose-boxed. Компиляция с этим параметром действует так, как если бы каждое объявление в модуле имело аннотацию @JvmExposeBoxed.
Унаследованные функции
Аннотация @JvmExposeBoxed не генерирует автоматически упакованные представления для унаследованных функций.
Чтобы сгенерировать необходимое представление для унаследованной функции, переопределите её в реализующем или производном классе:
interface IdTransformer {
fun transformId(rawId: UInt): UInt = rawId
}
// Doesn't generate a boxed representation for the transformId() function
@OptIn(ExperimentalStdlibApi::class)
@JvmExposeBoxed
class LightweightTransformer : IdTransformer
// Generates a boxed representation for the transformId() function
@OptIn(ExperimentalStdlibApi::class)
@JvmExposeBoxed
class DefaultTransformer : IdTransformer {
override fun transformId(rawId: UInt): UInt = super.transformId(rawId)
}
Подробнее о наследовании в Kotlin и вызове реализаций суперкласса с помощью ключевого слова super см. в разделе Наследование.
© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/java-to-kotlin-interop.html