Вызов кода Kotlin из Java

Код Kotlin может быть вызван из JAVA просто.

Свойства (Properties)

Свойство Kotlin компилируется в следующие Java элементы:

  • Метод getter, имя которого вычисляется путем добавления префикса get;
  • Метод setter, имя которого вычисляется путем добавления префикса set (только для свойств var);
  • Приватное поле с тем же именем, что и имя свойства (только для свойств с backing fields).

Например, var firstName: String компилируется в следующие объявления Java:

private String firstName;

public String getFirstName () {
     return firstName;
}

public void setFirstName (String firstName) {
     this.firstName = firstName;
}

Если имя свойства начинается с is, используется другое правило сопоставления имен: имя метода getter будет совпадать с именем свойства, а имя метода setter будет получено путем замены is на set. Например, для свойства isOpen, getter будет называться isOpen() и setter будет называться setOpen(). Это правило применяется к свойствам любого типа, а не только к Boolean.

Функции уровня пакета (Package-Level Functions)

Все функции и свойства, объявленные в файле example.kt внутри пакета org.foo.bar, включая функции расширения, скомпилированы в статические методы класса Java с именем org.foo.bar.ExampleKt.

// example.kt
package demo

class Foo

fun bar() { ... }

// Java
new demo.Foo();
demo.ExampleKt.bar();

Имя генерируемого JAVA класса может быть выбрано при помощи аннотации @JvmName:

@file:JvmName("DemoUtils")

package demo

class Foo

fun bar() { ... }

// Java
new demo.Foo();
demo.DemoUtils.bar();

Наличие нескольких файлов, имеющих одинаковое имя Java-класса (тот же пакет и одинаковое имя или аннотацию @JvmName), является ошибкой. Однако компилятор имеет возможность генерировать один Java класс фасада, имеющий указанное имя и содержащий все объявления из всех файлов, которые имеют такое имя. Чтобы включить генерацию такого фасада, используйте аннотацию @JvmMultifileClass во всех файлах.

// oldutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass

package demo

fun foo() { ... }
// newutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass

package demo

fun bar() { ... }
// Java
demo.Utils.foo();
demo.Utils.bar();

Поля экземпляра (Instance Fields)

Если вам нужно представить свойство Котлина в качестве поля в Java, вам нужно добавить к нему аннотацию @JvmField. Поле будет иметь такую же видимость, что и базовое свойство. Вы можете добавить свойству аннотацию @JvmField, если оно имеет backing field, не является приватным, не имеет open, override или const модификаторов и не является делегированным свойством.

class C(id: String) {
    @JvmField val ID = id
}
// Java
class JavaClient {
    public String getID(C c) {
        return c.ID;
    }
}

Свойства с поздней инициализацией также отображаются как поля. Видимость поля будет такой же, как видимость сеттера свойства с поздней инициализацией.

Статические поля (Static Fields)

Свойства Kotlin, объявленные в именованном объекте или объекте-помощнике, будут иметь статические backing fields в этом именованном объекте или в классе, содержащем объект-помощник.

Обычно эти поля являются приватными, но они могут быть представлены одним из следующих способов:

  • @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

Свойство с поздней инициализацией в объекте или объекте-помощнике имеет статическое backing field с той же видимостью, что и сеттер свойства.

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 c = Obj.CONST;
int d = ExampleKt.MAX;
int v = C.VERSION;

Статические методы (Static Methods)

Как упоминалось выше, Kotlin представляет функции уровня пакета как статические методы. Kotlin также может генерировать статические методы для функций, определенных в именованных объектах или объектах-помощниках, если вы добавите аннотацию @JvmStatic к функции. Если вы используете эту аннотацию, компилятор создаст как статический метод во включающем классе объекта, так и метод экземпляра в самом объекте. Например:

class C {
    companion object {
        @JvmStatic fun foo() {}
        fun bar() {}
    }
}

Теперь foo() является статическим методом в Java, в то время как bar() нет:

C.foo(); // works fine
C.bar(); // error: not a static method
C.Companion.foo(); // instance method remains
C.Companion.bar(); // the only way it works

То же самое для именованных объектов:

object Obj {
    @JvmStatic fun foo() {}
    fun bar() {}
}

В Java:

Obj.foo(); // works fine
Obj.bar(); // error
Obj.INSTANCE.bar(); // works, a call through the singleton instance
Obj.INSTANCE.foo(); // works too

Аннотацию @JvmStatic можно также применить к свойству объекта или объекта-помощника, сделав его методы getter и setter статическими элементами в этом объекте или классе, содержащем объект-помощник.

Видимость (Visibility)

Видимость в 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 Foo @JvmOverloads constructor(x: Int, y: Double = 0.0) {
    @JvmOverloads fun f(a: String, b: Int = 0, c: String = "abc") { ... }
}

Для каждого параметра со значением по умолчанию это создаст одну дополнительную перегрузку, которая имеет этот параметр и все параметры справа от него в удаленном списке параметров. В этом примере будет создано следующее:

// Constructors:
Foo(int x, double y)
Foo(int x)

// Methods
void f(String a, int b, String c) { }
void f(String a, int b) { }
void f(String a) { }

Обратите внимание, как описаны вторичные конструкторы, если класс имеет значения по умолчанию для всех параметров конструктора, то для него будет создан открытый конструктор без аргументов. Это работает, даже если не указана аннотация @JvmOverloads.

Проверенные исключения (Checked Exceptions)

Как мы уже упоминали выше, Котлин не имеет проверенных исключений. Таким образом, как правило, сигнатуры Java функций Kotlin не объявляют исключения. Поэтому если мы имеем такую функцию в Котлине:

// example.kt
package demo

fun foo() {
    throw IOException()
}

И мы хотим вызвать её из Java и поймать исключение:

// Java
try {
  demo.Example.foo();
}
catch (IOException e) { // error: foo() does not declare IOException in the throws list
  // ...
}

мы получаем сообщение об ошибке из компилятора Java, потому что foo() не объявляет IOException. Чтобы обойти эту проблему, используйте аннотацию @Throws в Kotlin:

@Throws(IOException::class)
fun foo() {
    throw IOException()
}

Null-безопасность (Null-safety)

При вызове Kotlin функций из Java никто не мешает нам передавать null{: .keyword} в качестве ненулевого параметра. Вот почему Kotlin генерирует проверки времени выполнения для всех публичных функций, которые ожидают непустые значения. Таким образом, мы немедленно получаем исключение NullPointerException в Java-коде.

Variant generics

Когда классы 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) { ... }

ПРИМЕЧАНИЕ. Когда тип аргумента является окончательным, обычно нет смысла создавать подстановочный знак, поэтому 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 (Translation of type Nothing)

Тип Nothing является особым, потому что он не имеет естественного аналога на Java. Действительно, каждый ссылочный тип Java, включая java.lang.Void, принимает значение null как значение, а Nothing не принимает даже этого. Таким образом, этот тип не может быть точно представлен в Java-мире. Вот почему Kotlin генерирует необработанный тип, где аргумент типа Nothing используется:

fun emptyList(): List<Nothing> = listOf()
// is translated to
// List emptyList() { ... }