Spec-Zone.ru › Kotlin 1.8

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

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

© 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