Guide d'interopérabilité Kotlin-Java

Ce document est un ensemble de règles pour la création d'API publiques en Java et Kotlin. avec l'intention de faire en sorte que le code soit idiomatique lorsqu'il est consommé langue.

Dernière mise à jour: 29/07/2024

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 champs. Ceux-ci nécessitent l’utilisation d’accents graves pour s’échapper lors de l’appel depuis Kotlin. Mots clés génériques, mots clés modificateurs et Les identifiants 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 ou les noms des propriétés d'extension sur Any pour sauf en cas d'absolue nécessité. Les méthodes et champs des membres ont priorité sur les fonctions ou propriétés d'extension de Any, elles peuvent être lors de la lecture du code, il est difficile de savoir lequel est appelé.

Annotations de possibilité de valeur nulle

Tout type de paramètre, de retour et de champ non primitif dans une API publique doit une annotation de possibilité de valeur nulle. Les types non annotés sont interprétés comme "plate-forme" , qui présentent une possibilité de valeur nulle ambiguë.

Par défaut, le compilateur Kotlin respecte les annotations JSR 305, mais les signale avec 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 Flowable.create() de RxJava 2 est définie comme suit:

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

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

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

Toutefois, si les paramètres ont été inversés dans la signature de la méthode, les appels de fonction pourrait 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 sous la forme d'une propriété en Kotlin, un attribut strict de type "bean" doit être utilisé.

Les méthodes d'accesseur nécessitent un préfixe get ou, pour les méthodes renvoyant une valeur booléenne, un 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()

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 tels que Accesseurs has, set ou sans préfixe get. Méthodes avec des préfixes non standards peuvent toujours être appelés en tant que fonctions, ce qui peut être acceptable en fonction de la le comportement de la méthode.

Surcharge de l'opérateur

Faites attention aux noms de méthodes qui autorisent une syntaxe de site d'appel spéciale (comme surcharge de l'opérateur en Kotlin). Assurez-vous que les méthodes s'appellent qu'il est judicieux de l'utiliser 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 est toujours annoté avec @file:JvmName("Foo") pour lui donner un joli nom.

Par défaut, les membres de premier niveau d'un fichier MyClass.kt se retrouvent dans une classe appelée MyClassKt, qui n'est pas attrayant et qui divulgue la langue lors de l'implémentation dans les moindres détails.

Envisagez d'ajouter @file:JvmMultifileClass pour combiner les membres de niveau supérieur à partir desquels plusieurs fichiers dans une seule classe.

Arguments lambda

Les interfaces à méthode unique (SAM, Single Method Interface) définies en Java peuvent être implémentées à la fois en Kotlin et Java à l'aide de la syntaxe lambda, qui intègre l'implémentation dans un langage de la même façon. Kotlin propose plusieurs options pour définir ces interfaces, chacune ayant une légère la différence.

Définition à privilégier

Fonctions d'ordre supérieur destinées à être utilisées à partir de Java Les types de fonction qui renvoient Unit ne doivent pas être acceptés nécessitent que les appelants Java renvoient Unit.INSTANCE. Au lieu d'intégrer la fonction saisissez la signature, utilisez des interfaces fonctionnelles (SAM). Aussi Envisagez d'utiliser des interfaces fonctionnelles (SAM) plutôt que des interfaces lors de la définition d'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 en est un bon exemple. car il est destiné à comparer this à other, et les lambdas n'ont pas de this. Non le préfixe fun de l'interface oblige l'appelant à utiliser object : ... qui lui permet d'avoir un état et de 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. Brut 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 dont vous n'êtes pas propriétaire en lecture seule à partir d'API publiques, encapsulez dans un conteneur non modifiable ou d’effectuer une copie défensive. Malgré Kotlin leur propriété en lecture seule, cette mesure n'est pas appliquée sur le côté. Sans le wrapper ou la copie défensive, les règles invariantes peuvent être enfreintes par 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. sous la forme d'une 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 sont uniquement disponibles d'instance "getters" sur le champ statique Companion. Utilisation de @JvmStatic à la place de @JvmField déplace les "getters" au nom étrange aux 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 créer des noms idiomatiques pour les conventions des deux langages ou pour correspondre à leur bibliothèque standard respective nommage.

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

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 qui ont du sens. Si ce n'est pas le cas, effectuez au moins l'une des refactorisations suivantes jusqu'à satisfaction:

  • Modifiez l'ordre des paramètres pour que ceux ayant des valeurs par défaut soient placés en priorité à 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 les problèmes d'interopérabilité décrits précédemment. Uniquement les problèmes en Java (pour Kotlin) de consommation) sont 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 à Fichier > Préférences > Éditeur > les 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 coché les règles que vous souhaitez activer, les nouvelles vérifications s'exécuter lorsque vous lancez vos inspections de code (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.