Leitfaden zur Kotlin-Java-Interoperabilität

Dieses Dokument enthält eine Reihe von Regeln für das Erstellen öffentlicher APIs in Java und Kotlin, damit sich der Code idiomatisch anfühlt, wenn er in der jeweils anderen Sprache verwendet wird.

Java (für Kotlin)

Keine harten Keywords

Verwenden Sie keine der festen Kotlin-Keywords als Namen für Methoden oder Felder. Dafür müssen Sie Backticks verwenden, um Escapezeichen zu setzen, wenn Sie sie aus Kotlin aufrufen. Weiche Keywords, Modifikator-Keywords und Sonderkennungen sind zulässig.

Für die Mockito-Funktion when sind beispielsweise Backticks erforderlich, wenn sie in Kotlin verwendet wird:

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

Any-Erweiterungsnamen vermeiden

Vermeiden Sie es, die Namen der Erweiterungsfunktionen für Any für Methoden oder die Namen der Erweiterungseigenschaften für Any für Felder zu verwenden, es sei denn, dies ist unbedingt erforderlich. Während Member-Methoden und -Felder immer Vorrang vor den Erweiterungsfunktionen oder ‑attributen von Any haben, kann es beim Lesen des Codes schwierig sein, zu erkennen, welche aufgerufen wird.

Annotationen zur Null-Zulässigkeit

Jeder nicht primitive Parameter, Rückgabe- und Feldtyp in einer öffentlichen API sollte eine Null-Zulässigkeit-Annotation haben. Nicht annotierte Typen werden als Plattformtypen interpretiert, deren Nullable-Status nicht eindeutig ist.

Standardmäßig berücksichtigt der Kotlin-Compiler JSR 305-Annotationen, kennzeichnet sie aber mit Warnungen. Sie können auch ein Flag festlegen, damit der Compiler die Anmerkungen als Fehler behandelt.

Lambda-Parameter zuletzt

Parametertypen, die für die SAM-Conversion infrage kommen, sollten zuletzt stehen.

Die Methodensignatur von Flowable.create() in RxJava 2 ist beispielsweise so definiert:

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

Da FlowableOnSubscribe für die SAM-Konvertierung infrage kommt, sehen Funktionsaufrufe dieser Methode in Kotlin so aus:

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

Wenn die Parameter in der Methodensignatur jedoch umgekehrt wären, könnten Funktionsaufrufe die Trailing-Lambda-Syntax verwenden:

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

Attributpräfixe

Damit eine Methode in Kotlin als Property dargestellt werden kann, muss ein striktes Präfix im „Bean“-Stil verwendet werden.

Für Zugriffsfunktionen ist das Präfix get erforderlich. Für Methoden, die einen booleschen Wert zurückgeben, kann das Präfix is verwendet werden.

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

Die zugehörigen Mutatormethoden erfordern das Präfix 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)

Wenn Sie Methoden als Attribute verfügbar machen möchten, verwenden Sie keine nicht standardmäßigen Präfixe wie has, set oder Accessoren ohne get-Präfix. Methoden mit nicht standardmäßigen Präfixen können weiterhin als Funktionen aufgerufen werden. Das kann je nach Verhalten der Methode akzeptabel sein.

Operator-Überladung

Achten Sie auf Methodennamen, die eine spezielle Aufrufsitesyntax zulassen, z. B. Operator-Überladung in Kotlin. Achten Sie darauf, dass die Methodennamen für die Verwendung mit der abgekürzten Syntax geeignet sind.

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 (für Java)

Dateiname

Wenn eine Datei Funktionen oder Eigenschaften auf oberster Ebene enthält, müssen Sie sie immer mit @file:JvmName("Foo") annotieren, um einen aussagekräftigen Namen anzugeben.

Standardmäßig werden Elemente der obersten Ebene in einer Datei „MyClass.kt“ in einer Klasse namens MyClassKt platziert. Das ist nicht gerade ansprechend und die Sprache wird als Implementierungsdetail offengelegt.

Fügen Sie @file:JvmMultifileClass hinzu, um die Elemente der obersten Ebene aus mehreren Dateien in einer einzigen Klasse zu kombinieren.

Lambda-Argumente

In Java definierte Schnittstellen mit einer einzigen Methode (Single Abstract Method, SAM) können sowohl in Kotlin als auch in Java mit der Lambda-Syntax implementiert werden, wodurch die Implementierung auf idiomatische Weise inline erfolgt. Kotlin bietet mehrere Optionen zum Definieren solcher Schnittstellen, die sich jeweils geringfügig unterscheiden.

Bevorzugte Definition

Funktionen höherer Ordnung, die in Java verwendet werden sollen, dürfen keine Funktionstypen mit dem Rückgabetyp Unit verwenden, da Java-Aufrufer dann Unit.INSTANCE zurückgeben müssten. Verwenden Sie funktionale (SAM-)Schnittstellen, anstatt den Funktionstyp in der Signatur zu inline. Erwägen Sie außerdem, funktionale (SAM-)Schnittstellen anstelle regulärer Schnittstellen zu verwenden, wenn Sie Schnittstellen definieren, die als Lambdas verwendet werden sollen. Dies ermöglicht eine idiomatische Verwendung in Kotlin.

Sehen Sie sich diese Kotlin-Definition an:

fun interface GreeterCallback {
  fun greetName(String name)
}

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

Wenn sie von Kotlin aus aufgerufen wird:

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

Wenn die Methode über Java aufgerufen wird:

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

Auch wenn der Funktionstyp kein Unit zurückgibt, kann es sinnvoll sein, ihn als benannte Schnittstelle zu definieren, damit Aufrufer ihn mit einer benannten Klasse und nicht nur mit Lambdas (in Kotlin und Java) implementieren können.

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

Funktionstypen vermeiden, die Unit zurückgeben

Sehen Sie sich diese Kotlin-Definition an:

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

Java-Aufrufer müssen Unit.INSTANCE zurückgeben:

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

Vermeiden Sie funktionale Schnittstellen, wenn die Implementierung einen Status haben soll.

Wenn die Schnittstellenimplementierung einen Status haben soll, ist die Verwendung der Lambda-Syntax nicht sinnvoll. Comparable ist ein gutes Beispiel, da this mit other verglichen werden soll und Lambdas kein this haben. Wenn der Schnittstelle kein fun vorangestellt wird, muss der Aufrufer die object : ...-Syntax verwenden, die einen Status ermöglicht und dem Aufrufer einen Hinweis gibt.

Sehen Sie sich diese Kotlin-Definition an:

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

Dadurch wird die Lambda-Syntax in Kotlin verhindert, sodass diese längere Version erforderlich ist:

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

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

Generika für Nothing vermeiden

Ein Typ, dessen generischer Parameter Nothing ist, wird Java als Rohdatentyp zur Verfügung gestellt. Rohdatentypen werden in Java selten verwendet und sollten vermieden werden.

Dokumentausnahmen

Funktionen, die geprüfte Ausnahmen auslösen können, sollten mit @Throws dokumentiert werden. Laufzeit-Exceptions sollten in KDoc dokumentiert werden.

Achten Sie auf die APIs, an die eine Funktion delegiert wird, da sie möglicherweise geprüfte Ausnahmen auslösen, die Kotlin ansonsten stillschweigend weiterleitet.

Defensive Kopien

Wenn Sie schreibgeschützte Sammlungen, die freigegeben oder nicht im Besitz sind, über öffentliche APIs zurückgeben, schließen Sie sie in einen nicht modifizierbaren Container ein oder erstellen Sie eine defensive Kopie. Obwohl Kotlin die schreibgeschützte Eigenschaft erzwingt, gibt es auf der Java-Seite keine solche Erzwingung. Ohne den Wrapper oder die defensive Kopie können Invarianten verletzt werden, indem eine langlebige Sammlung zurückgegeben wird.

Begleitfunktionen

Öffentliche Funktionen in einem Companion-Objekt müssen mit @JvmStatic annotiert werden, um als statische Methode verfügbar gemacht zu werden.

Ohne die Annotation sind diese Funktionen nur als Instanzmethoden für ein statisches Companion-Feld verfügbar.

Falsch: keine Anmerkung

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

Richtig:@JvmStatic Anmerkung

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

Companion-Konstanten

Öffentliche, nicht-const-Attribute, die effektive Konstanten in einem companion object sind, müssen mit @JvmField annotiert werden, um als statisches Feld verfügbar gemacht zu werden.

Ohne die Annotation sind diese Properties nur als ungewöhnlich benannte Instanz-„Getter“ für das statische Feld Companion verfügbar. Wenn Sie stattdessen @JvmStatic anstelle von @JvmField verwenden, werden die ungewöhnlich benannten „Getter“ in statische Methoden der Klasse verschoben, was immer noch falsch ist.

Falsch: keine Anmerkung

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

Falsch:@JvmStatic Anmerkung

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

Richtig:@JvmField Anmerkung

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

Idiomatische Benennung

Kotlin hat andere Aufrufkonventionen als Java, was sich auf die Benennung von Funktionen auswirken kann. Verwenden Sie @JvmName, um Namen so zu gestalten, dass sie für die Konventionen beider Sprachen idiomatisch sind oder mit der Namensgebung der jeweiligen Standardbibliothek übereinstimmen.

Das passiert am häufigsten bei Erweiterungsfunktionen und ‑attributen, da sich der Empfängertyp an einem anderen Ort befindet.

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

Funktionsüberladungen für Standardwerte

Bei Funktionen mit Parametern, die einen Standardwert haben, muss @JvmOverloads verwendet werden. Ohne diese Anmerkung ist es nicht möglich, die Funktion mit Standardwerten aufzurufen.

Wenn Sie @JvmOverloads verwenden, prüfen Sie die generierten Methoden, um sicherzustellen, dass sie alle sinnvoll sind. Wenn nicht, führen Sie eine oder beide der folgenden Refactoring-Vorgänge aus, bis Sie zufrieden sind:

  • Ändern Sie die Reihenfolge der Parameter so, dass die Parameter mit Standardwerten am Ende stehen.
  • Verschieben Sie die Standardwerte in manuelle Funktionsüberladungen.

Falsch: Nein @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");
    }
}

Richtig:@JvmOverloads Vermerk.

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

Lint-Prüfungen

Voraussetzungen

  • Android Studio-Version:3.2 Canary 10 oder höher
  • Android-Gradle-Plug-in-Version:3.2 oder höher

Unterstützte Prüfungen

Es gibt jetzt Android Lint-Prüfungen, mit denen Sie einige der zuvor beschriebenen Interoperabilitätsprobleme erkennen und kennzeichnen können. Es werden nur Probleme in Java (für die Verwendung in Kotlin) erkannt. Folgende Prüfungen werden unterstützt:

  • Unbekannte Nullheit
  • Property-Zugriff
  • Keine harten Kotlin-Keywords
  • Lambda-Parameter zuletzt

Android Studio

Wenn Sie diese Prüfungen aktivieren möchten, rufen Sie Datei > Einstellungen > Editor > Inspections auf und aktivieren Sie die Regeln, die Sie unter „Kotlin Interoperability“ aktivieren möchten:

Abbildung 1. Kotlin-Interoperabilitätseinstellungen in Android Studio.

Nachdem Sie die Regeln ausgewählt haben, die Sie aktivieren möchten, werden die neuen Prüfungen ausgeführt, wenn Sie die Codeüberprüfungen ausführen (Analyze > Inspect Code…).

Befehlszeilen-Builds

Wenn Sie diese Prüfungen für Befehlszeilen-Builds aktivieren möchten, fügen Sie die folgende Zeile in Ihre build.gradle-Datei ein:

Groovy

android {

    ...

    lintOptions {
        enable 'Interoperability'
    }
}

Kotlin

android {
    ...

    lintOptions {
        enable("Interoperability")
    }
}

Eine vollständige Liste der in „lintOptions“ unterstützten Konfigurationen finden Sie in der Gradle-DSL-Referenz für Android.

Führen Sie dann ./gradlew lint über die Befehlszeile aus.