Spec-Zone.ru › Kotlin 1.7

Вызов 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

Чтобы изменить имена сгенерированных методов доступа к свойствам без явной реализации методов getter и setter, вы можете использовать @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 не указана.

END_OF_DOCUMENT_MARKER

Исключения с проверкой

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