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

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

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

Нет точных ключевых слов

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

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

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

Избегайте Any дополнительных названий.

Избегайте использования имен функций расширения класса Any для методов или имен свойств расширения класса Any для полей, если это не является абсолютно необходимым. Хотя методы и поля-члены всегда будут иметь приоритет над функциями расширения или свойствами класса 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, необходимо использовать строгий префикс в стиле "бинов".

Для методов доступа требуется префикс 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)

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

Имя файла

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

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

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

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

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

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

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

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

fun interface GreeterCallback {
  fun greetName(String name)
}

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

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

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

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

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

Для этого требуется, чтобы Java-вызовы возвращали Unit.INSTANCE :

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

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

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

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

// 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 коллекций, доступных только для чтения (разделяемых или не принадлежащих кому-либо), их следует обернуть в неизменяемый контейнер или выполнить защитное копирование. Несмотря на то, что в Kotlin свойство "только для чтения" обеспечивается автоматически, в 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). В частности, поддерживаются следующие проверки:

  • Неизвестная Пустота
  • Доступ к собственности
  • Нет сложных ключевых слов Kotlin
  • Параметры лямбда-функции (последние)

Android Studio

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

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

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

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

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

Классный

android {

    ...

    lintOptions {
        enable 'Interoperability'
    }
}

Котлин

android {
    ...

    lintOptions {
        enable("Interoperability")
    }
}

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

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