Руководство по взаимодействию Kotlin-Java

Этот документ представляет собой набор правил для создания общедоступных API на Java и Kotlin с намерением сделать код идиоматическим при использовании с другого языка.

Последнее обновление: 29 июля 2024 г.

Java (для использования Kotlin)

Никаких жестких ключевых слов

Не используйте жесткие ключевые слова Kotlin в качестве названий методов или полей. Они требуют использования обратных кавычек для выхода из кода при вызове из Котлина. Допускаются мягкие ключевые слова , ключевые слова-модификаторы и специальные идентификаторы .

Например, функция when Mockito требует обратных кавычек при использовании из Kotlin:

val callable = Mockito.mock(Callable::class.java)
Mockito.`when`(callable.call()).thenReturn(/* … */)

Избегайте Any имен расширений

Избегайте использования имен функций расширения в методах Any for или имен свойств расширения в полях Any for без крайней необходимости. Хотя методы и поля-члены всегда будут иметь приоритет над функциями или свойствами расширения Any , при чтении кода может быть сложно узнать, какой из них вызывается.

Аннотации об отсутствии значений

Каждый не примитивный параметр, возвращаемый результат и тип поля в общедоступном API должны иметь аннотацию, допускающую значение NULL. Неаннотированные типы интерпретируются как «платформенные» типы , которые допускают неоднозначное значение NULL.

По умолчанию флаги компилятора Kotlin учитывают аннотации JSR 305, но помечают их предупреждениями. Вы также можете установить флаг, чтобы компилятор рассматривал аннотации как ошибки.

Лямбда-параметры в последнюю очередь

Типы параметров, подходящие для преобразования SAM, должны быть последними.

Например, сигнатура метода Flowable.create() в RxJava 2 определяется как:

public static <T> Flowable<T> create(
    FlowableOnSubscribe<T> source,
    BackpressureStrategy mode) { /* … */ }

Поскольку FlowableOnSubscribe поддерживает преобразование SAM, вызовы функций этого метода из Kotlin выглядят следующим образом:

Flowable.create({ /* … */ }, BackpressureStrategy.LATEST)

Однако если бы параметры в сигнатуре метода были изменены местами, вызовы функций могли бы использовать синтаксис завершающих лямбда-выражений:

Flowable.create(BackpressureStrategy.LATEST) { /* … */ }

Префиксы свойств

Чтобы метод был представлен как свойство в Kotlin, необходимо использовать строгие префиксы в стиле «bean».

Для методов доступа требуется префикс get , а для методов, возвращающих логические значения, можно использовать префикс is .

public final class User {
  public String getName() { /* … */ }
  public boolean isActive() { /* … */ }
}
val name = user.name // Invokes user.getName()
val active = user.isActive // Invokes user.isActive()

Связанные методы-мутаторы требуют префикса set .

public final class User {
  public String getName() { /* … */ }
  public void setName(String name) { /* … */ }
  public boolean isActive() { /* … */ }
  public void setActive(boolean active) { /* … */ }
}
user.name = "Bob" // Invokes user.setName(String)
user.isActive = true // Invokes user.setActive(boolean)

Если вы хотите, чтобы методы были представлены как свойства, не используйте нестандартные префиксы, такие как методы доступа с префиксом has , set или без префикса get . Методы с нестандартными префиксами по-прежнему можно вызывать как функции, что может быть приемлемо в зависимости от поведения метода.

Перегрузка оператора

Помните об именах методов, которые допускают специальный синтаксис места вызова (например, перегрузку операторов в Kotlin). Убедитесь, что имена методов как таковые имеют смысл использовать с сокращенным синтаксисом.

public final class IntBox {
  private final int value;
  public IntBox(int value) {
    this.value = value;
  }
  public IntBox plus(IntBox other) {
    return new IntBox(value + other.value);
  }
}
val one = IntBox(1)
val two = IntBox(2)
val three = one + two // Invokes one.plus(two)

Котлин (для использования Java)

Имя файла

Если файл содержит функции или свойства верхнего уровня, всегда добавляйте к нему аннотацию @file:JvmName("Foo") чтобы обеспечить красивое имя.

По умолчанию члены верхнего уровня в файле MyClass.kt попадают в класс MyClassKt , который непривлекательный и допускает утечку языка в качестве детали реализации.

Рассмотрите возможность добавления @file:JvmMultifileClass , чтобы объединить члены верхнего уровня из нескольких файлов в один класс.

Лямбда-аргументы

Интерфейсы одного метода (SAM), определенные в Java, могут быть реализованы как в Kotlin, так и в Java с использованием лямбда-синтаксиса, который встраивает реализацию идиоматическим образом. В Kotlin есть несколько вариантов определения таких интерфейсов, каждый из которых имеет небольшую разницу.

Предпочтительное определение

Функции высшего порядка , которые предназначены для использования из Java, не должны принимать типы функций , возвращающие Unit поскольку для этого потребуется, чтобы вызывающие Java возвращали Unit.INSTANCE . Вместо встраивания типа функции в сигнатуру используйте функциональные (SAM) интерфейсы . Также рассмотрите возможность использования функциональных (SAM) интерфейсов вместо обычных интерфейсов при определении интерфейсов, которые, как ожидается, будут использоваться в качестве лямбда-выражений, что позволяет использовать идиоматическое использование Kotlin.

Рассмотрим это определение Котлина:

fun interface GreeterCallback {
  fun greetName(String name)
}

fun sayHi(greeter: GreeterCallback) = /* … */

При вызове из Котлина:

sayHi { println("Hello, $it!") }

При вызове из Java:

sayHi(name -> System.out.println("Hello, " + name + "!"));

Даже если тип функции не возвращает Unit все равно может быть хорошей идеей сделать его именованным интерфейсом, чтобы позволить вызывающим сторонам реализовать его с помощью именованного класса, а не только лямбда-выражений (как в Kotlin, так и в Java).

class MyGreeterCallback : GreeterCallback {
  override fun greetName(name: String) {
    println("Hello, $name!");
  }
}

Избегайте типов функций, которые возвращают Unit

Рассмотрим это определение Котлина:

fun sayHi(greeter: (String) -> Unit) = /* … */

Он требует, чтобы вызывающие программы Java возвращали Unit.INSTANCE :

sayHi(name -> {
  System.out.println("Hello, " + name + "!");
  return Unit.INSTANCE;
});

Избегайте функциональных интерфейсов, когда реализация должна иметь состояние.

Если реализация интерфейса должна иметь состояние, использование лямбда-синтаксиса не имеет смысла. Ярким примером является Comparable , поскольку он предназначен для сравнения this с other , а в лямбдах this нет. Отсутствие префикса fun в интерфейсе вынуждает вызывающую сторону использовать синтаксис object : ... , который позволяет ему иметь состояние, предоставляя подсказку вызывающей стороне.

Рассмотрим это определение Котлина:

// No "fun" prefix.
interface Counter {
  fun increment()
}

Это предотвращает лямбда-синтаксис в Kotlin, требуя более длинную версию:

runCounter(object : Counter {
  private var increments = 0 // State

  override fun increment() {
    increments++
  }
})

Nothing избегайте дженериков

Тип, универсальным параметром которого является Nothing , предоставляется Java как необработанные типы. Необработанные типы редко используются в Java, и их следует избегать.

Исключения для документов

Функции, которые могут генерировать проверенные исключения, должны документировать их с помощью @Throws . Исключения времени выполнения должны быть задокументированы в KDoc.

Помните об API, которым делегирует функция, поскольку они могут генерировать проверенные исключения, которые в противном случае Kotlin позволяет молча распространять.

Защитные копии

При возврате общих или бесхозных коллекций, доступных только для чтения, из общедоступных API, поместите их в неизменяемый контейнер или выполните защитное копирование. Несмотря на то, что Котлин применяет свойство «только для чтения», на стороне Java такого соблюдения нет. Без оболочки или защитной копии инварианты могут быть нарушены путем возврата долгоживущей ссылки на коллекцию.

Сопутствующие функции

Открытые функции в сопутствующем объекте должны быть помечены @JvmStatic , чтобы их можно было использовать как статический метод.

Без аннотации эти функции доступны только как методы экземпляра статического поля Companion .

Неправильно: нет аннотации.

class KotlinClass {
    companion object {
        fun doWork() {
            /* … */
        }
    }
}
public final class JavaClass {
    public static void main(String... args) {
        KotlinClass.Companion.doWork();
    }
}

Правильно: аннотация @JvmStatic

class KotlinClass {
    companion object {
        @JvmStatic fun doWork() {
            /* … */
        }
    }
}
public final class JavaClass {
    public static void main(String... args) {
        KotlinClass.doWork();
    }
}

Сопутствующие константы

Открытые, const свойства, которые являются эффективными константами в companion object должны быть помечены @JvmField , чтобы их можно было представить как статическое поле.

Без аннотации эти свойства доступны только как «получатели» экземпляра со странным именем в статическом поле Companion . Использование @JvmStatic вместо @JvmField перемещает «геттеры» со странными именами в статические методы класса, что по-прежнему неверно.

Неправильно: нет аннотации.

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        val BIG_INTEGER_ONE = BigInteger.ONE
    }
}
public final class JavaClass {
    public static void main(String... args) {
        System.out.println(KotlinClass.INTEGER_ONE);
        System.out.println(KotlinClass.Companion.getBIG_INTEGER_ONE());
    }
}

Неверно: аннотация @JvmStatic

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        @JvmStatic val BIG_INTEGER_ONE = BigInteger.ONE
    }
}
public final class JavaClass {
    public static void main(String... args) {
        System.out.println(KotlinClass.INTEGER_ONE);
        System.out.println(KotlinClass.getBIG_INTEGER_ONE());
    }
}

Правильно: аннотация @JvmField

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        @JvmField val BIG_INTEGER_ONE = BigInteger.ONE
    }
}
public final class JavaClass {
    public static void main(String... args) {
        System.out.println(KotlinClass.INTEGER_ONE);
        System.out.println(KotlinClass.BIG_INTEGER_ONE);
    }
}

Идиоматическое именование

В Kotlin действуют другие соглашения о вызовах, чем в Java, что может изменить способ именования функций. Используйте @JvmName чтобы создавать имена так, чтобы они выглядели идиоматично для соглашений обоих языков или соответствовали именам соответствующих стандартных библиотек.

Чаще всего это происходит с функциями расширения и свойствами расширения, поскольку расположение типа получателя отличается.

sealed class Optional<T : Any>
data class Some<T : Any>(val value: T): Optional<T>()
object None : Optional<Nothing>()

@JvmName("ofNullable")
fun <T> T?.asOptional() = if (this == null) None else Some(this)
// FROM KOTLIN:
fun main(vararg args: String) {
    val nullableString: String? = "foo"
    val optionalString = nullableString.asOptional()
}
// FROM JAVA:
public static void main(String... args) {
    String nullableString = "Foo";
    Optional<String> optionalString =
          Optionals.ofNullable(nullableString);
}

Перегрузки функций для значений по умолчанию

Функции с параметрами, имеющими значение по умолчанию, должны использовать @JvmOverloads . Без этой аннотации невозможно вызвать функцию, используя любые значения по умолчанию.

При использовании @JvmOverloads проверьте сгенерированные методы, чтобы убедиться, что каждый из них имеет смысл. Если это не так, выполните один или оба следующих рефакторинга, пока не будете удовлетворены:

  • Измените порядок параметров, чтобы отдать предпочтение тем, у которых значения по умолчанию находятся ближе к концу.
  • Переместите значения по умолчанию в перегрузки функций вручную.

Неверно: нет @JvmOverloads

class Greeting {
    fun sayHello(prefix: String = "Mr.", name: String) {
        println("Hello, $prefix $name")
    }
}
public class JavaClass {
    public static void main(String... args) {
        Greeting greeting = new Greeting();
        greeting.sayHello("Mr.", "Bob");
    }
}

Правильно: аннотация @JvmOverloads .

class Greeting {
    @JvmOverloads
    fun sayHello(prefix: String = "Mr.", name: String) {
        println("Hello, $prefix $name")
    }
}
public class JavaClass {
    public static void main(String... args) {
        Greeting greeting = new Greeting();
        greeting.sayHello("Bob");
    }
}

Проверка на наличие ворса

Требования

  • Версия Android Studio: 3.2 Canary 10 или новее.
  • Версия плагина Android Gradle: 3.2 или новее.

Поддерживаемые проверки

Теперь существуют проверки Android Lint, которые помогут вам обнаружить и отметить некоторые проблемы совместимости, описанные ранее. Обнаруживаются только проблемы в Java (для использования Kotlin). В частности, поддерживаются следующие проверки:

  • Неизвестная нуль
  • Доступ к собственности
  • Нет ключевых слов Hard Kotlin
  • Лямбда-параметры последние

Android-студия

Чтобы включить эти проверки, перейдите в меню «Файл» > «Настройки» > «Редактор» > «Проверки» и отметьте правила, которые вы хотите включить в разделе «Взаимодействие с Kotlin»:

Рисунок 1. Настройки совместимости Kotlin в Android Studio.

После того как вы проверите правила, которые хотите включить, новые проверки будут выполняться при проверке кода ( Анализ > Проверить код… ).

Сборки из командной строки

Чтобы включить эти проверки из сборок командной строки, добавьте следующую строку в файл build.gradle :

классный

android {

    ...

    lintOptions {
        enable 'Interoperability'
    }
}

Котлин

android {
    ...

    lintOptions {
        enable("Interoperability")
    }
}

Полный набор конфигураций, поддерживаемых внутри lintOptions, можно найти в справочнике Android Gradle DSL .

Затем запустите ./gradlew lint из командной строки.