Spec-Zone.ru › Kotlin 1.6

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

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.

В случае наследования от 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. Таким образом, мы получаем 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) { ... }

Когда тип аргумента является конечным, обычно нет смысла генерировать уайлдкард, поэтому 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() { ... }
Последнее изменение: 07 апреля 2022
Вызов Java из Kotlin Создание RESTful веб-сервиса с базой данных с помощью Spring Boot – учебник

© 2010–2022 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