Przewodnik interoperacyjności Kotlin-Java

Ten dokument to zbiór reguł tworzenia publicznych interfejsów API w języku Java i Kotlin, który ma sprawić, że po użyciu kodu z innego języka może on zostać uznany za idiomatyczny.

Ostatnia aktualizacja: 18.05.2018 r.

Java (do konsumpcji Kotlin)

Brak twardych słów kluczowych

Nie używaj żadnych twardych słów kluczowych Kotlin jako nazwy metod czy pól. Wymaga to użycia grawisu, aby uciec, gdy wywołujesz Kotlin. Dozwolone są miękkie słowa kluczowe, słowa kluczowe modyfikujące i specjalne identyfikatory.

Na przykład funkcja when Mockito wymaga grawisów, gdy jest używany z Kotlin:

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

Unikaj nazw rozszerzeń Any

Jeśli nie jest to absolutnie konieczne, unikaj używania w przypadku metod nazw funkcji rozszerzeń w Any lub nazw właściwości rozszerzeń w Any w polach. Chociaż metody i pola składowe zawsze mają pierwszeństwo przed funkcjami lub właściwościami rozszerzenia Any, określenie, które z nich jest wywoływane, może być trudne podczas odczytywania kodu.

Adnotacje z wartością null

Każdy parametr, zwrot i typ pola w publicznym interfejsie API powinien mieć adnotację z możliwością wartości null. Typy bez adnotacji są interpretowane jako typy „platform”, które mają niejednoznaczną wartość null.

Domyślnie flagi kompilatora Kotlin uznają adnotacje JSR 305, ale oznaczają je ostrzeżeniami. Możesz też ustawić flagę, dzięki której kompilator będzie traktować adnotacje jako błędy.

Ostatnie parametry Lambda

Typy parametrów, które kwalifikują się do konwersji SAM, powinny być ostatnie.

Na przykład podpis metody RxJava 2’s Flowable.create() jest zdefiniowany jako:

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

Ponieważ FlowableOnSubskrybuj kwalifikuje się do konwersji SAM, wywołania funkcji tej metody z Kotlin wyglądają tak:

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

Gdyby jednak parametry zostały odwrócone w podpisie metody, wywołania funkcji mogą używać składni lambda na końcu:

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

Prefiksy usługi

Aby metoda została przedstawiona jako właściwość w Kotlin, należy użyć ścisłego prefiksu w stylu „bean”.

Metody dostępu wymagają prefiksu „get”, a w przypadku metod zwracających wartości logiczne można użyć prefiksu „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()

Powiązane metody mutatora wymagają prefiksu „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)

Jeśli chcesz ujawnić metody jako właściwości, nie używaj niestandardowych prefiksów, takich jak „hasła”, „set” czy „get” (nie pobieraj). Metody z niestandardowymi prefiksami nadal można wywoływać jako funkcje, które mogą być dopuszczalne w zależności od ich działania.

Przeciążenie operatora

Zwróć uwagę na nazwy metod, które umożliwiają użycie specjalnej składni witryny wywołań (np. przeciążania operatorów) w Kotlin. Upewnij się, że takie nazwy metod mają sens w przypadku ich skróconej składni.

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 (do użytku w języku Java)

Nazwa pliku

Jeśli plik zawiera funkcje lub właściwości najwyższego poziomu, zawsze dodawaj do niego adnotacje @file:JvmName("Foo"), aby zapewnić ładną nazwę.

Domyślnie członkowie najwyższego poziomu w pliku MyClass.kt znajdą się w klasie o nazwie MyClassKt, która jest nieatrakcyjna i przecieka język jako szczegóły implementacji.

Rozważ dodanie @file:JvmMultifileClass, aby połączyć w 1 klasie użytkowników najwyższego poziomu z wielu plików.

Argumenty lambda

Interfejsów pojedynczej metody (SAM) zdefiniowanych w języku Java można implementować zarówno w kotlinach, jak i w Javie, korzystając ze składni lambda, która implementuje się w sposób idiomatyczny. Kotlin ma kilka opcji definiowania takich interfejsów, z których każda różni się nieco od siebie.

Preferowana definicja

Funkcje wyższego rzędu, które mają być używane w Javie, nie powinny przyjmować typów funkcji, które zwracają Unit, ponieważ wymagają one zwracania wartości Unit.INSTANCE przez elementy wywołujące Java. Zamiast umieszczać typ funkcji w podpisie, używaj interfejsów funkcjonalnych (SAM). Do definiowania interfejsów, które mają być używane jako lambda, rozważ też stosowanie interfejsów funkcjonalnych (SAM) zamiast zwykłych. Pozwoli to na idiomatyczne użycie usługi Kotlin.

Przeanalizujmy tę definicję Kotlina:

fun interface GreeterCallback {
  fun greetName(String name)
}

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

Po wywołaniu z Kotlin:

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

Po wywołaniu z Javy:

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

Nawet jeśli typ funkcji nie zwraca wartości Unit, warto stworzyć interfejs w formie nazwanej, aby umożliwić elementom wywołującym go implementowanie za pomocą nazwanej klasy, a nie tylko lambda (w kotlinach i w języku Java).

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

Unikaj typów funkcji, które zwracają wartość Unit

Przeanalizujmy tę definicję Kotlina:

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

Wymaga to od wywołań Javy funkcji Unit.INSTANCE:

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

Unikaj używania funkcjonalnych interfejsów, gdy implementacja ma mieć stan

Jeśli implementacja interfejsu ma mieć stan, używanie składni lambda nie ma sensu. Przykładem jest porównywalny, ponieważ służy do porównywania wartości this z other, a lambda nie ma parametru this. Brak prefiksu fun wymusza użycie składni object : ..., która umożliwia używanie stanu przez element wywołujący.

Przeanalizujmy tę definicję Kotlina:

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

Zapobiega składniom lambda w Kotlin, wymagając tej dłuższej wersji:

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

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

Unikaj nazw ogólnych (Nothing)

Typ, którego parametr ogólny to Nothing, jest dostępny w języku Java jako typy nieprzetworzone. Nieprzetworzone typy są rzadko używane w Javie i należy ich unikać.

Wyjątki dla dokumentów

Funkcje, które mogą zgłaszać zaznaczone wyjątki, powinny udokumentować je za pomocą funkcji @Throws. Wyjątki środowiska wykonawczego powinny być udokumentowane w KDoc.

Pamiętaj o interfejsach API, do których deleguje funkcja, ponieważ mogą one zgłaszać zaznaczone wyjątki, które Kotlin zezwala na ich rozpowszechnianie dyskretnie.

Kopie obronne

Gdy zwracasz z publicznych interfejsów API udostępnione lub nienależące do Ciebie kolekcje, umieść je w kontenerze, którego nie można zmienić, lub wykonaj kopię obronną. Chociaż Kotlin egzekwuje swoją właściwość tylko do odczytu, nie jest to egzekwowane po stronie Javy. Bez kodu obronnego i kodu defensywnego niezmienność może zostać naruszona przez zwrócenie długoterminowego odniesienia do kolekcji.

Funkcje towarzyszące

Aby funkcje publiczne w obiekcie towarzyszącym były widoczne jako metoda statyczna, muszą być oznaczone adnotacją @JvmStatic.

Bez adnotacji te funkcje są dostępne tylko jako metody instancji w statycznym polu Companion.

Nieprawidłowo: brak adnotacji

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

Prawidłowa adnotacja: @JvmStatic adnotacja

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

Stałe towarzyszące

Publiczne właściwości innych niż const, które są efektywnymi stałymi w elemencie companion object, muszą mieć adnotację @JvmField, aby były widoczne jako pole statyczne.

Bez adnotacji te właściwości są dostępne tylko jako instancja o dziwnej nazwie „getters” w statycznym polu Companion. Użycie @JvmStatic zamiast @JvmField przenosi dziwne nazwy „getters” do metod statycznych klasy, co nadal jest nieprawidłowe.

Nieprawidłowo: brak adnotacji

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());
    }
}

Niepoprawna: adnotacja @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());
    }
}

Prawidłowa adnotacja: @JvmField adnotacja

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);
    }
}

Idiomatyczne nazwy

Kotlin ma inne konwencje wywoływania niż Java, które mogą zmieniać sposób nadawania nazw funkcjom. Użyj @JvmName, aby projektować nazwy w taki sposób, aby były idiomatyczne w obu konwencjach językowych lub pasowało do odpowiednich standardowych nazw bibliotek.

Dzieje się tak najczęściej w przypadku funkcji rozszerzeń i właściwości rozszerzeń, ponieważ lokalizacja typu odbiorcy jest inna.

sealed class Optional
data class Some(val value: T): Optional()
object None : Optional()

@JvmName("ofNullable")
fun  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 optionalString =
          Optionals.ofNullable(nullableString);
}

Przeciążenie funkcji w przypadku wartości domyślnych

Funkcje, których parametry mają wartość domyślną, muszą używać parametru @JvmOverloads. Bez tej adnotacji nie można wywołać funkcji z użyciem jakichkolwiek wartości domyślnych.

Jeśli używasz metody @JvmOverloads, sprawdź wygenerowane metody, aby upewnić się, że każda ma sens. Jeśli nie, wykonaj jedną lub obie z tych refaktoryzacji do momentu, aż spełnią Twoje oczekiwania:

  • Zmień kolejność parametrów, aby preferować te, których wartości domyślne znajdują się bliżej końca.
  • Przenieś wartości domyślne do ręcznych przeciążeń funkcji.

Nieprawidłowo: nie @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");
    }
}

Prawidłowa adnotacja: @JvmOverloads adnotacja.

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");
    }
}

Likwidacja szczotek

Wymagania

  • Wersja Android Studio: 3.2 Canary 10 lub nowsza
  • Wersja wtyczki Androida do obsługi Gradle: 3.2 lub nowsza

Obsługiwane testy

Dostępne są teraz testy Android Lint, które pomogą Ci wykrywać i zgłaszać niektóre z opisanych wyżej problemów ze zgodnością. Obecnie wykrywane są tylko problemy w Javie (do wykorzystania Kotlin). Obsługiwane są te kwestie:

  • Nieznana wartość null
  • Dostęp do usługi
  • Brak twardych słów kluczowych Kotlin
  • Ostatnie parametry Lambda

Android Studio

Aby włączyć te weryfikacje, kliknij Plik > Ustawienia > Edytor > Inspekcje i w sekcji interoperacyjności Kotlin sprawdź reguły, które chcesz włączyć:

Rysunek 1. Ustawienia interoperacyjności Kotlin w Android Studio.

Gdy zaznaczysz reguły, które chcesz włączyć, nowe testy będą przeprowadzane po uruchomieniu inspekcji kodu (Analiza > Sprawdź kod...).

Kompilacje z poziomu wiersza poleceń

Aby włączyć te kontrole w kompilacjach wiersza poleceń, dodaj ten wiersz do pliku build.gradle:

Odlotowy

android {

    ...

    lintOptions {
        enable 'Interoperability'
    }
}

Kotlin

android {
    ...

    lintOptions {
        enable("Interoperability")
    }
}

Pełną listę konfiguracji obsługiwanych w lintOptions znajdziesz w dokumentacji DSL w Androidzie Gradle.

Następnie uruchom polecenie ./gradlew lint z poziomu wiersza poleceń.