Вызов 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 const = 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 также может быть применена к свойству объекта или объекта-компаньона, делая его методы-геттер и -сеттер статическими членами в этом объекте или классе, содержащем объект-компаньон.
Методы по умолчанию в интерфейсах
Методы по умолчанию доступны только для целей JVM 1.8 и выше.
Начиная с 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 1.4 для генерации методов по умолчанию можно было использовать аннотацию
@JvmDefaultна этих методах. Компиляция с-Xjvm-default=allв 1.4 обычно работает так, как если бы вы аннотировали все неабстрактные методы интерфейсов с@JvmDefaultи скомпилировали с-Xjvm-default=enable. Однако существуют случаи, когда их поведение отличается. Подробная информация о изменениях в генерации методов по умолчанию в Kotlin 1.4 приведена в этой статье на блоге Kotlin.
Режим совместимости для методов по умолчанию
Если у вас есть клиенты, которые используют ваши интерфейсы Kotlin, скомпилированные без новой опции -Xjvm-default=all , то они могут быть несовместимы с тем же кодом, скомпилированным с этой опцией.
Чтобы избежать нарушения совместимости с такими клиентами, скомпилируйте свой код Kotlin в режиме совместимости, указав опцию компилятора -Xjvm-default=all-compatibility. В этом случае весь код, использующий предыдущую версию, будет работать нормально с новой. Однако режим совместимости добавляет некоторую нагрузку на размер полученного байт-кода.
Для новых интерфейсов нет необходимости рассматривать совместимость, так как до этого их не использовали ни какие клиенты. Вы можете минимизировать нагрузку совместимости, исключив эти интерфейсы из режима совместимости. Для этого добавьте к ним аннотацию @JvmDefaultWithoutCompatibility. Такие интерфейсы компилируются так же, как и с -Xjvm-default=all.
Кроме того, в режиме all-compatibility вы можете использовать @JvmDefaultWithoutCompatibility для аннотирования всех интерфейсов, которые не экспортируются в общедоступный API и поэтому не используются существующими клиентами.
Видимость
Модификаторы видимости 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 нет проверяемых исключений. Таким образом, обычно, подписи Java функций 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("s")), но в 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) { ... }
Когда тип аргумента является final, обычно нет смысла генерировать подстановочный знак, поэтому
Box<String>всегдаBox<String>, независимо от того, какую позицию он занимает.
Если нам нужны подстановочные знаки там, где они по умолчанию не генерируются, мы можем использовать аннотацию @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) { ... }
@JvmSuppressWildcardsможет использоваться не только для отдельных аргументов типа, но и для целых объявлений, таких как функции или классы, подавляя все подстановочные знаки внутри них.
Перевод типа Nothing
Тип Nothing является особым, потому что у него нет естественного аналога в Java. Действительно, каждый тип ссылки Java, включая java.lang.Void, принимает null в качестве значения, а Nothing не принимает даже это. Поэтому этот тип не может быть точно представлен в мире Java. Вот почему Kotlin генерирует сырой тип, где используется аргумент типа Nothing:
fun emptyList(): List<Nothing> = listOf()
// is translated to
// List emptyList() { ... }
© 2010–2020 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html