Spec-Zone.ru › Kotlin 1.9

Вызов 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 также может быть применена к свойству объекта или объекта-компаньона, делая его методы получения и установки статическими членами в этом объекте или классе, содержащем объект-компаньон.

END_OF_DOCUMENT_MARKER

Методы по умолчанию в интерфейсах

Методы по умолчанию доступны только для целей 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 , то они могут быть бинарно несовместимы с кодом, скомпилированным с этим параметром. Чтобы избежать нарушения совместимости с такими клиентами, используйте режим -Xjvm-default=all и пометьте интерфейсы аннотацией @JvmDefaultWithCompatibility. Это позволит вам добавить эту аннотацию ко всем интерфейсам в публичном API один раз, и вам не нужно будет использовать какие-либо аннотации для нового закрытого кода.

Начиная с Kotlin 1.6.20, вы можете компилировать модули в режиме по умолчанию (параметр компилятора -Xjvm-default=disable) против модулей, скомпилированных с режимами -Xjvm-default=all или -Xjvm-default=all-compatibility.

Дополнительную информацию о режимах совместимости:

disable

Поведение по умолчанию. Не генерировать JVM методы по умолчанию и запретить использование аннотации @JvmDefault.

all

Генерировать JVM методы по умолчанию для всех объявлений интерфейсов с телами в модуле. Не генерировать DefaultImpls заглушки для объявлений интерфейсов с телами, которые генерируются по умолчанию в режиме disable.

Если интерфейс наследует метод с телом из интерфейса, скомпилированного в режиме disable и не переопределяет его, то будет сгенерирована DefaultImpls заглушка.

Нарушает бинарную совместимость, если некоторый клиентский код полагается на наличие DefaultImpls классов.

Если используется делегирование интерфейса, все методы интерфейса делегируются. Исключение составляют методы, помеченные устаревшей аннотацией @JvmDefault.

all-compatibility

В дополнение к режиму all генерировать заглушки совместимости в классах DefaultImpls. Заглушки совместимости могут быть полезны для авторов библиотек и исполняемых файлов, чтобы сохранить обратную бинарную совместимость для существующих клиентов, скомпилированных против предыдущих версий библиотек. all и all-compatibility режимы изменяют поверхность ABI библиотеки, которую будут использовать клиенты после повторной компиляции библиотеки. В этом смысле клиенты могут быть несовместимы с предыдущими версиями библиотек. Это обычно означает, что вам нужна надлежащая версия библиотеки, например, увеличение основной версии в SemVer.

Компилятор генерирует все члены DefaultImpls с аннотацией @Deprecated: вы не должны использовать эти члены в коде Java, потому что компилятор генерирует их только для целей совместимости.

В случае наследования от интерфейса Kotlin, скомпилированного в режимах all или all-compatibility , DefaultImpls заглушки совместимости вызовут метод по умолчанию интерфейса со стандартной семантикой разрешения времени выполнения JVM.

Выполнять дополнительные проверки совместимости для классов, наследующих обобщенные интерфейсы, где в некоторых случаях были сгенерированы дополнительные неявные методы со специализированными сигнатурами в режиме 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 нет проверочных исключений. Поэтому подписи 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 во время выполнения для всех публичных функций, которые ожидают не-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) { ... }

Когда тип аргумента является 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() { ... }
Последнее изменение: 28 сентября 2022
Вызов Java из Kotlin Начало работы со Spring Boot и Kotlin

© 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API