Guide d'interopérabilité Kotlin-Java

Ce document est un ensemble de règles permettant de créer des API publiques en Java et Kotlin dans le but de rendre le code idiomatique lorsqu'il est utilisé dans l'autre langage.

Dernière mise à jour : 18-05-2018

Java (pour une consommation en Kotlin)

Pas de mots clés exacts

N'utilisez aucun des mots clés exacts de Kotlin comme nom de méthode ou de champ. Ceux-ci nécessitent l'utilisation de backticks pour les échapper lors d'un appel depuis Kotlin. Les mots clés approximatifs, les mots clés modificateurs et les identificateurs spéciaux sont autorisés.

Par exemple, la fonction when de Mockito nécessite des accents graves lorsqu'elle est utilisée depuis Kotlin :

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

Éviter les noms d'extension Any

Évitez d'utiliser les noms des fonctions d'extension sur Any pour les méthodes ou des propriétés d'extension sur Any pour les champs, à moins que cela ne soit indispensable. Bien que les méthodes et les champs member aient toujours la priorité sur les fonctions ou les propriétés d'extension Any, il peut être difficile de savoir laquelle est appelée en lisant le code.

Annotations de possibilité de valeur nulle

Tout type de paramètre, de renvoi et de champ non primitif dans une API publique doit comporter une annotation de possibilité de valeur nulle. Les types non annotés sont interprétés comme des types "plate-forme", dont la possibilité de valeur nulle est ambigüe.

Par défaut, le compilateur Kotlin respecte les annotations JSR 305, mais les signale par des avertissements. Vous pouvez également définir un indicateur pour que le compilateur traite les annotations comme des erreurs.

Paramètres lambda en dernier

Les types de paramètres éligibles à la conversion SAM doivent être situés en dernier.

Par exemple, la signature de la méthode RxJava 2’s Flowable.create() est définie comme suit :

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

Comme FlowableOnSubscriber est éligible à la conversion SAM, les appels de fonction de cette méthode depuis Kotlin se présentent comme suit :

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

Cependant, si les paramètres étaient inversés dans le protocole de la méthode, les appels de fonction pourraient utiliser la syntaxe du lambda de fin :

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

Préfixes de propriété

Pour qu'une méthode soit représentée en tant que propriété dans Kotlin, un préfixe strict de type "bean" doit être utilisé.

Les méthodes d'accesseur nécessitent un préfixe "get". Pour les méthodes qui renvoient une valeur booléenne, un préfixe "is" peut être utilisé.

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

Les méthodes de mutateur associées nécessitent un préfixe "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)

Si vous souhaitez que les méthodes soient exposées en tant que propriétés, n'utilisez pas de préfixes non standards comme "has"/"set" ou des accesseurs sans préfixe "get". Les méthodes comportant des préfixes non standards peuvent toujours être appelées en tant que fonctions, ce qui peut être acceptable en fonction de leur comportement.

Surcharge de l'opérateur

Faites attention aux noms de méthodes qui autorisent une syntaxe de site d'appel spéciale (c'est-à-dire, une surcharge d'opérateur) dans Kotlin. Assurez-vous que les noms des méthodes sont pertinents pour une utilisation avec la syntaxe raccourcie.

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 (pour une consommation en Java)

Nom du fichier

Lorsqu'un fichier contient des fonctions ou des propriétés de niveau supérieur, il faut toujours l'annoter avec @file:JvmName("Foo") pour lui donner un nom attrayant.

Par défaut, les membres de niveau supérieur d'un fichier MyClass.kt se retrouvent dans une classe appelée MyClassKt, qui n'est pas attrayante et qui divulgue le langage en tant que détail d'implémentation.

Pensez à ajouter @file:JvmMultifileClass pour regrouper les membres de niveau supérieur de plusieurs fichiers en une seule classe.

Arguments lambda

Les types de fonctions destinés à être utilisés à partir de Java doivent éviter le type renvoyé Unit. Cela nécessite de spécifier une instruction return Unit.INSTANCE; explicite et non idiomatique.

fun sayHi(callback: (String) -> Unit) = /* … */
// Kotlin caller:
greeter.sayHi { Log.d("Greeting", "Hello, $it!") }
// Java caller:
greeter.sayHi(name -> {
    Log.d("Greeting", "Hello, " + name + "!");
    return Unit.INSTANCE;
});

Cette syntaxe ne permet pas non plus de fournir un type nommé sémantiquement qui puisse être implémenté sur d'autres types.

Définir une interface SAM (Single-abstraction-Method, ou méthode abstraite unique en français) nommée en Kotlin pour le type lambda corrige le problème pour Java, mais empêche l'utilisation de la syntaxe lambda en Kotlin.

interface GreeterCallback {
    fun greetName(name: String): Unit
}

fun sayHi(callback: GreeterCallback) = /* … */
// Kotlin caller:
greeter.sayHi(object : GreeterCallback {
    override fun greetName(name: String) {
        Log.d("Greeting", "Hello, $name!")
    }
})
// Java caller:
greeter.sayHi(name -> Log.d("Greeting", "Hello, " + name + "!"))

Définir une interface SAM nommée en Java permet d'utiliser une version légèrement inférieure de la syntaxe lambda de Kotlin, dans laquelle le type d'interface doit être spécifié explicitement.

// Defined in Java:
interface GreeterCallback {
    void greetName(String name);
}
fun sayHi(greeter: GreeterCallback) = /* … */
// Kotlin caller:
greeter.sayHi(GreeterCallback { Log.d("Greeting", "Hello, $it!") })
// Java caller:
greeter.sayHi(name -> Log.d("Greeter", "Hello, " + name + "!"));

À l'heure actuelle, il n'existe aucun moyen de définir un type de paramètre à utiliser comme lambda depuis Java et Kotlin de sorte qu'il soit idiomatique dans les deux langages. La recommandation actuelle consiste à privilégier le type de fonction malgré la dégradation de l'expérience depuis Java lorsque le type renvoyé est Unit.

Éviter les génériques Nothing

Les types dont le paramètre générique est Nothing sont exposés en tant que types bruts en Java. Les types bruts sont rarement utilisés en Java et doivent être évités.

Documenter les exceptions

Les fonctions qui peuvent générer des exceptions vérifiées doivent les documenter avec @Throws. Les exceptions d'exécution doivent être documentées dans KDoc.

Faites attention aux API auxquelles une fonction délègue, car elles peuvent générer des exceptions vérifiées que Kotlin peut sinon diffuser en silence.

Copies défensives

Lorsque vous renvoyez des collections partagées ou ne vous appartenant pas en lecture seule à partir d'API publiques, encapsulez-les dans un conteneur non modifiable ou effectuez une copie défensive. Bien que Kotlin applique la propriété de lecture seule, il n'en va pas de même du côté de Java. Sans le wrapper ou la copie défensive, les règles invariantes peuvent être ignorées en renvoyant une référence de collection de longue durée.

Fonctions des compagnons

Les fonctions publiques d'un objet compagnon doivent être annotées avec @JvmStatic pour être exposées en tant que méthode statique.

Sans l'annotation, ces fonctions ne sont disponibles que comme méthodes d'instance sur un champ Companion statique.

Incorrect : aucune annotation

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

Correct : annotation @JvmStatic

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

Constantes des compagnons

Les propriétés publiques non const qui sont des constantes effectives dans un companion object doivent être annotées avec @JvmField pour être exposées en tant que champ statique.

Sans l'annotation, ces propriétés ne sont disponibles qu'en tant que "getters" d'instance sur le champ statique Companion. L'utilisation de @JvmStatic au lieu de @JvmField déplace ces getters vers des méthodes statiques de la classe, ce qui est toujours incorrect.

Incorrect : aucune annotation

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

Incorrect : annotation @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());
    }
}

Correct : annotation @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);
    }
}

Noms idiomatiques

Kotlin utilise des conventions d'appel différentes de celles de Java, qui peuvent modifier la façon dont vous nommez les fonctions. Utilisez @JvmName pour définir des noms qui soient idiomatiques pour les conventions des deux langages ou qui correspondent aux noms de bibliothèque standard respectifs.

Cela se produit le plus souvent pour les fonctions et les propriétés d'extension, car l'emplacement du type de récepteur est différent.

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

Surcharges de fonctions pour les valeurs par défaut

Les fonctions avec des paramètres ayant une valeur par défaut doivent utiliser @JvmOverloads. Sans cette annotation, il est impossible d'appeler la fonction à l'aide de valeurs par défaut.

Lorsque vous utilisez @JvmOverloads, inspectez les méthodes générées pour vous assurer qu'elles sont pertinentes. Si ce n'est pas le cas, effectuez l'une ou les deux refactorisations suivantes jusqu'à satisfaction :

  • Modifiez l'ordre des paramètres de sorte que ceux qui ont des valeurs par défaut soient situés vers la fin.
  • Déplacez les valeurs par défaut vers des surcharges de fonctions manuelles.

Incorrect : pas de @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");
    }
}

Correct : annotation @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");
    }
}

Vérifications lint

Conditions requises

  • Android Studio 3.2 Canary 10 ou version ultérieure
  • Plug-in Android Gradle 3.2 ou version ultérieure

Vérifications prises en charge

Des vérifications Android Lint vous permettent désormais de détecter et de signaler certains des problèmes d'interopérabilité décrits ci-dessus. Seuls les problèmes en Java (pour une consommation en Kotlin) sont actuellement détectés. Plus précisément, les vérifications prises en charge sont les suivantes :

  • Nullité inconnue
  • Accès aux propriétés
  • Aucun mot clé exact Kotlin
  • Paramètres lambda en dernier

Android Studio

Pour activer ces vérifications, accédez à File > Preferences > Editor > Inspections (Fichier > Préférences > Éditeur > Inspections) et cochez les règles que vous souhaitez activer sous Interopérabilité Kotlin :

Figure 1. Paramètres d'interopérabilité Kotlin dans Android Studio.

Une fois que vous avez vérifié les règles que vous souhaitez activer, les nouvelles vérifications s'exécutent lorsque vous lancez vos inspections de code en cliquant sur Analyze > Inspect Code… (Analyser > Inspecter le code…).

Builds de ligne de commande

Pour activer ces vérifications à partir des builds de ligne de commande, ajoutez la ligne suivante dans votre fichier build.gradle :

Groovy

android {

    ...

    lintOptions {
        enable 'Interoperability'
    }
}

Kotlin

android {
    ...

    lintOptions {
        enable("Interoperability")
    }
}

Pour obtenir la liste complète des configurations compatibles avec lintOptions, consultez la documentation de référence DSL Gradle pour Android.

Ensuite, exécutez ./gradlew lint à partir de la ligne de commande.