Вызов 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 представляет функции на уровне пакета в виде статических методов. Kotlin также может генерировать статические методы для функций, определенных в именованных объектах или объектах-компаньонах, если вы аннотируете эти функции как @JvmStatic. Если вы используете эту аннотацию, компилятор сгенерирует как статический метод в окружающем классе объекта, так и метод экземпляра в самом объекте. Например:
class C {
companion object {
@JvmStatic fun callStatic() {}
fun callNonStatic() {}
}
}
Теперь callStatic() является статическим в Java, в то время как callNonStatic() нет:
C.callStatic(); // works fine C.callNonStatic(); // error: not a static method C.Companion.callStatic(); // instance method remains C.Companion.callNonStatic(); // the only way it works
То же самое для именованных объектов:
object Obj {
@JvmStatic fun callStatic() {}
fun callNonStatic() {}
}
В Java:
Obj.callStatic(); // works fine Obj.callNonStatic(); // error Obj.INSTANCE.callNonStatic(); // works, a call through the singleton instance Obj.INSTANCE.callStatic(); // works too
Начиная с Kotlin 1.3, аннотация @JvmStatic применяется и к функциям, определенным в объектах-компаньонах интерфейсов, также компилируясь в статические методы в интерфейсах. Обратите внимание, что статические методы в интерфейсах были введены в Java 1.8, поэтому убедитесь, что используете соответствующие целевые версии.
interface ChatBot {
companion object {
@JvmStatic fun greet(username: String) {
println("Hello, $username")
}
}
}
Аннотация @JvmStatic также может быть применена к свойству объекта или объекта-компаньона, делая его методы получения и установки статическими членами в этом объекте или классе, содержащем объект-компаньон.
Методы по умолчанию в интерфейсах
Начиная с JDK 1.8, интерфейсы в Java могут содержать методы по умолчанию. Чтобы сделать все неабстрактные члены интерфейсов Kotlin методами по умолчанию для Java-классов, реализующих их, скомпилируйте код Kotlin с параметром компилятора -Xjvm-default=all.
Вот пример интерфейса Kotlin с методом по умолчанию:
// compile with -Xjvm-default=all
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, скомпилированные без параметра -Xjvm-default=all , то они могут быть бинарно несовместимы с кодом, скомпилированным с этим параметром. Чтобы избежать нарушения совместимости с такими клиентами, используйте режим -Xjvm-default=all и пометьте интерфейсы аннотацией @JvmDefaultWithCompatibility. Это позволяет вам добавить эту аннотацию ко всем интерфейсам в общедоступном API один раз, и вам не нужно использовать какие-либо аннотации для нового закрытого кода.
Узнайте больше о режимах совместимости:
disable
Поведение по умолчанию. Не генерировать JVM-методы по умолчанию и запретить использование аннотации @JvmDefault.
all
Генерировать JVM-методы по умолчанию для всех объявлений интерфейсов с телами в модуле. Не генерировать DefaultImpls заглушки для объявлений интерфейсов с телами, которые генерируются по умолчанию в режиме disable.
Если интерфейс наследует метод с телом из интерфейса, скомпилированного в режиме disable , и не переопределяет его, то для него будет сгенерирована DefaultImpls заглушка.
Нарушает бинарную совместимость, если некоторый клиентский код полагается на наличие DefaultImpls классов.
all-compatibility
В дополнение к режиму all генерировать заглушки совместимости в классах DefaultImpls. Заглушки совместимости могут быть полезны для авторов библиотек и сред выполнения, чтобы сохранить обратную бинарную совместимость для существующих клиентов, скомпилированных против предыдущих версий библиотек. all и all-compatibility режимы изменяют поверхность ABI библиотеки, которую будут использовать клиенты после повторной компиляции библиотеки. В этом смысле клиенты могут быть несовместимы с предыдущими версиями библиотек. Обычно это означает, что вам нужна надлежащая версия библиотеки, например, увеличение основной версии в SemVer.
Компилятор генерирует все члены DefaultImpls с аннотацией @Deprecated: вы не должны использовать эти члены в Java-коде, потому что компилятор генерирует их только для целей совместимости.
В случае наследования от интерфейса Kotlin, скомпилированного в режимах all или all-compatibility , DefaultImpls заглушки совместимости будут вызывать метод по умолчанию интерфейса с использованием стандартной семантики разрешения JVM runtime.
Выполнять дополнительные проверки совместимости для классов, наследующих обобщённые интерфейсы, где в некоторых случаях был сгенерирован дополнительный неявный метод со специализированными сигнатурами в режиме disable : в отличие от режима disable , компилятор будет сообщать об ошибке, если вы не переопределите такой метод явно и не аннотируете класс аннотацией @JvmDefaultWithoutCompatibility (см. эту проблему YouTrack для получения более подробной информации).
Видимость
Модификаторы видимости Kotlin отображаются в Java следующим образом:
Члены
privateкомпилируются в членыprivateТоп-уровневые объявления
privateкомпилируются в объявления уровня пакета.protectedостаётсяprotected(обратите внимание, что Java позволяет обращаться к защищённым членам из других классов в том же пакете, а Kotlin — нет, поэтому у Java-классов будет более широкий доступ к коду).Объявления
internalстановятсяpublicв Java. Члены классовinternalпроходят манипуляцию именами, чтобы затруднить случайное их использование из Java и разрешить перегрузку для членов с одинаковой сигнатурой, которые не видят друг друга в соответствии с правилами Kotlin.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 только как полная сигнатура со всеми присутствующими параметрами. Если вы хотите экспонировать несколько перегрузок вызывающим сторонам Java, вы можете использовать аннотацию @JvmOverloads.
Аннотация также работает для конструкторов, статических методов и так далее. Она не может использоваться для абстрактных методов, включая методы, определённые в интерфейсах.
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) { }
Обратите внимание, что, как описано в Вторичных конструкторах, если класс имеет значения по умолчанию для всех параметров конструктора, для него будет сгенерирован публичный конструктор без аргументов. Это работает даже если аннотация @JvmOverloads не указана.
Проверяемые исключения
В Kotlin нет проверяемых исключений. Поэтому в сигнатурах функций Kotlin обычно не объявляются выбрасываемые исключения. Таким образом, если у вас есть функция в 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. Чтобы обойти эту проблему, используйте аннотацию @Throws в Kotlin:
@Throws(IOException::class)
fun writeToFile() {
/*...*/
throw IOException()
}
Безопасность при работе с null
При вызове функций Kotlin из Java никто не запрещает передавать null в качестве параметра, не допускающего null. Вот почему Kotlin генерирует проверки во время выполнения для всех публичных функций, которые ожидают не-null. Таким образом, мы сразу получаем 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 это будет невозможно, потому что в Java класс Box является инвариантным по параметру 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() { ... }
© 2010–2023 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