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 d'accents graves 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 la signature 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 interfaces à méthode unique (SAM, Single Method Interface) définies en Java peuvent être implémentées en Kotlin et Java à l'aide de la syntaxe lambda, qui intègre l'implémentation de manière idiomatique. Kotlin propose plusieurs options pour définir de telles interfaces, chacune avec une légère différence.

Définition à privilégier

Les fonctions d'ordre supérieur destinées à être utilisées à partir de Java ne doivent pas utiliser de types de fonctions qui renvoient Unit comme cela serait le cas. Les appelants Java doivent renvoyer Unit.INSTANCE. Au lieu d'intégrer le type de fonction dans la signature, utilisez des interfaces fonctionnelles (SAM). Pensez également à utiliser des interfaces fonctionnelles (SAM) plutôt que des interfaces standards lorsque vous définissez des interfaces censées être utilisées en tant que lambdas, ce qui permet une utilisation idiomatique de Kotlin.

Prenons cette définition de Kotlin :

fun interface GreeterCallback {
  fun greetName(String name)
}

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

En cas d'appel depuis Kotlin :

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

En cas d'appel depuis Java :

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

Même si le type de fonction ne renvoie pas de Unit, il peut être judicieux d'en faire une interface nommée pour permettre aux appelants de l'implémenter avec une classe nommée et pas seulement des lambdas (à la fois dans Kotlin, et Java).

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

Éviter les types de fonction qui renvoient Unit

Prenons cette définition de Kotlin :

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

Les appelants Java doivent renvoyer Unit.INSTANCE :

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

Éviter les interfaces fonctionnelles lorsque l'implémentation est censée comporter un état

Lorsque l'implémentation de l'interface est censée avoir un état, l'utilisation de la syntaxe lambda n'est pas judicieuse. Comparable est un exemple important, car il est destiné à comparer this à other, et les lambdas n'ont pas de this. Ne pas préfixer l'interface avec fun oblige l'appelant à utiliser la syntaxe object : ..., ce qui lui permet d'avoir un état et fournir un indice à l'appelant.

Prenons cette définition de Kotlin :

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

Elle empêche la syntaxe lambda en Kotlin, ce qui nécessite cette version plus longue :

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

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

É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.