Leitfaden zur Kotlin-Java-Interoperabilität

Dieses Dokument enthält Regeln für die Entwicklung öffentlicher APIs in Java und Kotlin mit dem Ziel, dass der Code in der anderen Sprache idiomatisch wirkt.

Letzte Aktualisierung: 18.05.2018

Java (für Kotlin-Nutzung)

Keine festen Keywords

Verwenden Sie keine harten Schlüsselwörter aus Kotlin als Namen von Methoden oder Feldern. Bei Aufrufen aus Kotlin ist dafür die Verwendung von Gravis als Escapezeichen erforderlich. Softe Keywords, Keywords mit Modifizierer und spezielle Kennungen sind zulässig.

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

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

Keine Any-Erweiterungsnamen verwenden

Verwenden Sie die Namen der Erweiterungsfunktionen in Any nicht für Methoden oder die Namen der Erweiterungsattribute in Any für Felder, sofern dies nicht unbedingt erforderlich ist. Mitgliedsmethoden und -felder haben zwar immer Vorrang vor den Erweiterungsfunktionen oder -eigenschaften von Any, aber beim Lesen des Codes kann es schwierig sein, zu erkennen, welche aufgerufen wird.

Annotationen zur Null-Zulässigkeit

Für alle nicht-primitiven Parameter, Rückgaben und Feldtypen in einer öffentlichen API sollte eine Annotation zur Null-Zulässigkeit vorhanden sein. Nicht annotierte Typen werden als Plattformtypen interpretiert, die mehrdeutige Null-Zulässigkeit haben.

Standardmäßig berücksichtigen die Kotlin-Compiler-Flags JSR 305-Annotationen, kennzeichnen sie jedoch mit Warnungen. Sie können auch ein Flag festlegen, damit der Compiler die Annotationen als Fehler behandelt.

Lambda-Parameter zuletzt

Parametertypen, die für die SAM-Konvertierung infrage kommen, müssen zuletzt verwendet werden.

Die Methodensignatur RxJava 2’s Flowable.create() ist beispielsweise so definiert:

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

Da FlowableOnSubscribe für SAM-Konvertierung infrage kommt, sehen Funktionsaufrufe dieser Methode aus Kotlin wie folgt aus:

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

Würden die Parameter in der Methodensignatur jedoch umgekehrt, können Funktionsaufrufe die nachgestellte 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.

Zugriffsmethoden erfordern ein „get“-Präfix oder für boolesche Rückgabemethoden kann ein „is“-Präfix 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()

Verknüpfte Mutatormethoden erfordern ein "set"-Präfix.

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 möchten, dass Methoden als Eigenschaften verfügbar gemacht werden, verwenden Sie keine nicht standardmäßigen Präfixe wie „has“/„set“ oder andere Zugriffsmethoden mit dem Präfix „get“. Methoden mit nicht standardmäßigen Präfixen können dennoch als Funktionen aufgerufen werden, die je nach Verhalten der Methode akzeptabel sein können.

Überlastung durch Operator

Achten Sie auf Methodennamen, die eine spezielle Syntax für Aufrufwebsites in Kotlin zulassen (z.B. Überlastung durch Operatoren). Stellen Sie sicher, dass Methodennamen als solche mit der gekürzten Syntax sinnvoll 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-Nutzung)

Dateiname

Wenn eine Datei Funktionen oder Eigenschaften der obersten Ebene enthält, sollten Sie sie immer mit @file:JvmName("Foo") annotieren, um einen schönen Namen zu erhalten.

Standardmäßig gelangen Mitglieder der obersten Ebene in einer Datei MyClass.kt in eine Klasse namens MyClassKt, die nicht ansprechend ist und die Sprache als Implementierungsdetail leakt.

Sie können @file:JvmMultifileClass hinzufügen, um die Mitglieder der obersten Ebene aus mehreren Dateien in einer einzigen Klasse zusammenzufassen.

Lambda-Argumente

In Java definierte Single-Method Interfaces (SAM) können mithilfe der Lambda-Syntax sowohl in Kotlin als auch in Java implementiert werden. Dabei wird die Implementierung idiomatisch inline implementiert. 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, sollten keine Funktionstypen verwenden, die Unit zurückgeben, da Java-Aufrufer dafür erforderlich sind, Unit.INSTANCE zurückzugeben. Verwenden Sie funktionale SAM-Schnittstellen, anstatt den Funktionstyp in die Signatur einzufügen. Erwägen Sie auch die Verwendung von funktionalen Schnittstellen (SAM) anstelle von regulären Schnittstellen, wenn Sie Schnittstellen definieren, die voraussichtlich als Lambdas verwendet werden. Dies ermöglicht eine idiomatische Nutzung aus Kotlin.

Betrachten Sie diese Kotlin-Definition:

fun interface GreeterCallback {
  fun greetName(String name)
}

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

Bei Aufruf über Kotlin:

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

Bei Aufruf aus Java:

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

Auch wenn der Funktionstyp kein Unit zurückgibt, ist es möglicherweise dennoch eine gute Idee, ihn als benannte Schnittstelle zu verwenden, damit Aufrufer sie mit einer benannten Klasse und nicht nur mit Lambdas (sowohl in Kotlin als auch in Java) implementieren können.

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

Funktionstypen vermeiden, die Unit zurückgeben

Betrachten Sie diese Kotlin-Definition:

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

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

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

Funktionale Schnittstellen vermeiden, wenn die Implementierung einen Status haben soll

Wenn die Schnittstellenimplementierung einen Zustand haben soll, ist die Verwendung der Lambda-Syntax nicht sinnvoll. Vergleichbar ist ein auffälliges Beispiel, da es this mit other vergleichen soll und Lambdas nicht this haben. Wenn der Schnittstelle fun nicht vorangestellt wird, wird der Aufrufer gezwungen, die Syntax object : ... zu verwenden, die einen Status zulässt und dem Aufrufer einen Hinweis gibt.

Betrachten Sie diese Kotlin-Definition:

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

Sie verhindert Lambda-Syntax in Kotlin und erfordert diese längere Version:

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

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

Nothing-Generika vermeiden

Ein Typ mit dem generischen Parameter Nothing wird als Rohtypen für Java verfügbar gemacht. Raw-Typen werden in Java selten verwendet und sollten vermieden werden.

Dokumentausnahmen

Funktionen, die überprüfte Ausnahmen auslösen können, sollten mit @Throws dokumentiert werden. Laufzeitausnahmen sollten in KDoc dokumentiert werden.

Achten Sie auf die APIs, an die eine Funktion delegiert, da sie geprüfte Ausnahmen auslösen können, die von Kotlin ansonsten unbemerkt weitergegeben werden können.

Abwehrkopien

Wenn Sie freigegebene oder unbeanspruchte schreibgeschützte Sammlungen von öffentlichen APIs zurückgeben, verpacken Sie sie in einen nicht änderbaren Container oder führen Sie eine defensive Kopie aus. Obwohl Kotlin die schreibgeschützte Property erzwingt, gibt es auf Java-Seite keine solche Erzwingung. Ohne den Wrapper oder die Defensivkopie können Invarianten durch Rückgabe einer langlebigen Sammlungsreferenz verletzt werden.

Companion-Funktionen

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

Ohne die Annotation sind diese Funktionen nur als Instanzmethoden in einem statischen 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 Attribute, die keine const sind, die effektive Konstanten in einer companion object sind, müssen mit @JvmField annotiert werden, um als statisches Feld verfügbar gemacht zu werden.

Ohne die Annotation sind diese Attribute nur als Instanz-Getter mit seltsamen Namen im statischen Feld Companion verfügbar. Wenn Sie @JvmStatic anstelle von @JvmField verwenden, werden die seltsam benannten "Getter" in statische Methoden der Klasse verschoben. Dies ist immer noch falsch.

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 die Art der Benennung von Funktionen ändern kann. Verwenden Sie @JvmName, um Namen so zu gestalten, dass sie sowohl für die Sprachkonventionen als auch für die Benennung der jeweiligen Standardbibliothek idiomatisch erscheinen.

Das ist am häufigsten bei Erweiterungsfunktionen und Erweiterungseigenschaften der Fall, weil sich der Empfängertyp jeweils von einem anderen Standort unterscheidet.

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

Funktionsüberladungen für Standardeinstellungen

Funktionen mit Parametern, die einen Standardwert haben, müssen @JvmOverloads verwenden. Ohne diese Annotation ist es unmöglich, die Funktion mit Standardwerten aufzurufen.

Prüfen Sie bei Verwendung von @JvmOverloads, ob die generierten Methoden jeweils sinnvoll sind. Ist dies nicht der Fall, führen Sie eine oder beide der folgenden Refaktorierungen durch, bis Sie zufrieden sind:

  • Ändern Sie die Reihenfolge der Parameter so, dass die Standardparameter am Ende bevorzugt werden.
  • Verschieben Sie die Standardwerte in manuelle Funktionsüberlastungen.

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-Anmerkung

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

Fusselprüfungen

Voraussetzungen

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

Unterstützte Prüfungen

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

  • Unbekannte NULL-Werte
  • Property-Zugriff
  • Keine festen Kotlin-Schlüsselwörter
  • Lambda-Parameter zuletzt

Android Studio

Rufen Sie Datei > Einstellungen > Editor > Inspektionen auf und prüfen Sie unter „Kotlin-Interoperabilität“ die Regeln, die Sie aktivieren möchten:

Abbildung 1: Kotlin-Interoperabilitätseinstellungen in Android Studio

Nachdem Sie die Regeln geprüft haben, die Sie aktivieren möchten, werden die neuen Prüfungen bei der Codeprüfung (Analysieren > Code prüfen...) ausgeführt.

Befehlszeilen-Builds

Fügen Sie der Datei build.gradle die folgende Zeile hinzu, um diese Prüfungen über die Befehlszeilen-Builds zu aktivieren:

Groovig

android {

    ...

    lintOptions {
        enable 'Interoperability'
    }
}

Kotlin

android {
    ...

    lintOptions {
        enable("Interoperability")
    }
}

Die vollständigen Konfigurationen, die in lintOptions unterstützt werden, finden Sie in der Gradle-DSL-Referenz für Android.

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