Guida all'interoperabilità di Kotlin-Java

Questo documento è un insieme di regole per la creazione di API pubbliche in Java e Kotlin, allo scopo di fare in modo che il codice risulti idiomatico se utilizzato dall'altro linguaggio.

Ultimo aggiornamento: 18-05-2018

Java (per il consumo di Kotlin)

Nessuna parola chiave rigida

Non utilizzare nessuna parola chiave rigida di Kotlin come nome di metodi o campi. Questi richiedono l'uso di apici inversi per l'escape quando chiami da Kotlin. Sono consentiti parole chiave flessibili, parole chiave di modifica e identificatori speciali.

Ad esempio, la funzione when di Mockito richiede un apice inverso quando viene utilizzata da Kotlin:

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

Evita Any nomi di estensioni

Evita di utilizzare i nomi delle funzioni di estensione su Any per i metodi o i nomi delle proprietà delle estensioni su Any per i campi, a meno che non sia assolutamente necessario. Sebbene i metodi e i campi dei membri abbiano sempre la precedenza sulle funzioni o sulle proprietà dell'estensione di Any, sapere quale viene chiamato quando si legge il codice può essere difficile.

Annotazioni per l'abilità di valori null

Ogni parametro, ritorno e tipo di campo non primitivi in un'API pubblica deve avere un'annotazione con valore nulla. I tipi non annotati vengono interpretati come tipi"platform", con valori ambigui dei valori null.

Per impostazione predefinita, i flag del compilatore Kotlin rispettano le annotazioni JSR 305, ma li segnala con avvisi. Puoi anche impostare un flag in modo che il compilatore consideri le annotazioni come errori.

Ultimi parametri lambda

I tipi di parametri idonei per la conversione SAM devono essere gli ultimi.

Ad esempio, la firma del metodo RxJava 2’s Flowable.create() è definita come:

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

Poiché FlowableOnAbbonati è idoneo per la conversione SAM, le chiamate di funzione di questo metodo da Kotlin hanno il seguente aspetto:

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

Se i parametri fossero invertiti nella firma del metodo, tuttavia, le chiamate di funzione potrebbero utilizzare la sintassi lambda finale:

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

Prefissi proprietà

Affinché un metodo venga rappresentato come proprietà in Kotlin, è necessario utilizzare un prefisso rigoroso di tipo "bean".

I metodi della funzione di accesso richiedono un prefisso "get" oppure, per i metodi con restituzione booleana, è possibile utilizzare un prefisso "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()

I metodi mutatori associati richiedono un prefisso "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)

Se vuoi che i metodi siano visualizzati come proprietà, non utilizzare prefissi non standard come "has"/"set" o le funzioni di accesso senza prefisso "get". I metodi con prefissi non standard sono comunque richiamabili come funzioni che possono essere accettabili a seconda del comportamento del metodo.

Sovraccarico dell'operatore

Fai attenzione ai nomi dei metodi che consentono una sintassi speciale del sito di chiamata (ovvero, sovraccarico dell'operatore) in Kotlin. Assicurati che i nomi dei metodi come tali abbiano senso utilizzare con una sintassi abbreviata.

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 (per l'utilizzo di Java)

Nome file

Quando un file contiene funzioni o proprietà di primo livello, aggiungi sempre un'annotazione con @file:JvmName("Foo") per fornire un nome accattivante.

Per impostazione predefinita, i membri di primo livello in un file MyClass.kt finiranno in una classe denominata MyClassKt che è sgradevole e trapela la lingua come dettaglio di implementazione.

Valuta la possibilità di aggiungere @file:JvmMultifileClass per combinare i membri di primo livello provenienti da più file in un unico corso.

Argomenti lambda

Le interfacce a metodo singolo (SAM) definite in Java possono essere implementate sia in Kotlin che in Java utilizzando la sintassi lambda, che integra l'implementazione in modo idiomatico. Kotlin offre diverse opzioni per la definizione di queste interfacce, ognuna con una leggera differenza.

Definizione preferita

Le funzioni di ordine superiore destinate all'uso da Java non devono accettare i tipi di funzione che restituiscono Unit, poiché questo richiederebbe ai chiamanti Java di restituire Unit.INSTANCE. Anziché incorporare il tipo di funzione nella firma, utilizza le interfacce funzionali (SAM). Prendi in considerazione l'utilizzo di interfacce funzionali (SAM) anziché normali quando definisci quelle che dovrebbero essere utilizzate come lambda, il che consente un utilizzo idiomatico da Kotlin.

Considera questa definizione di Kotlin:

fun interface GreeterCallback {
  fun greetName(String name)
}

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

Quando richiamato da Kotlin:

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

Quando richiamato da Java:

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

Anche quando il tipo di funzione non restituisce Unit, potrebbe essere comunque una buona idea creare un'interfaccia con nome per consentire ai chiamanti di implementarla con una classe con nome e non solo con lambdas (sia in Kotlin che in Java).

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

Evita i tipi di funzione che restituiscono Unit

Considera questa definizione di Kotlin:

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

Richiede che i chiamanti Java restituiscano Unit.INSTANCE:

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

Evita le interfacce funzionali quando l'implementazione deve avere uno stato

Quando l'implementazione dell'interfaccia deve avere uno stato, l'utilizzo della sintassi lambda non ha senso. Comparabile è un esempio evidente, in quanto intende confrontare this con other, mentre le lambda non hanno this. Se non anteponi il prefisso fun all'interfaccia, il chiamante può utilizzare la sintassi object : ..., che può avere lo stato, fornendo un suggerimento al chiamante.

Considera questa definizione di Kotlin:

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

Impedisce la sintassi lambda in Kotlin, richiedendo questa versione più lunga:

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

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

Evita Nothing termini generici

Un tipo il cui parametro generico è Nothing è esposto come tipi non elaborati a Java. I tipi non elaborati vengono utilizzati raramente in Java e devono essere evitati.

Eccezioni per i documenti

Le funzioni che possono generare eccezioni selezionate devono documentarle con @Throws. Le eccezioni di runtime devono essere documentate in KDoc.

Fai attenzione alle API a cui delega una funzione, in quanto potrebbero generare eccezioni selezionate che Kotlin altrimenti consente di propagare in modo invisibile all'utente.

Copie difensive

Quando restituisci raccolte di sola lettura condivise o senza proprietà da API pubbliche, includile in un container non modificabile o esegui una copia difensiva. Nonostante Kotlin abbia applicato la sua proprietà di sola lettura, non esiste un'applicazione di questo tipo sul lato Java. Senza il wrapper o il testo difensivo, le informazioni invarianti possono essere violate restituendo un riferimento alla raccolta di lunga durata.

Funzioni di associazione

Le funzioni pubbliche in un oggetto companion devono essere annotate con @JvmStatic per essere esposte come metodo statico.

Senza l'annotazione, queste funzioni sono disponibili solo come metodi di istanza in un campo Companion statico.

Risposta sbagliata: nessuna annotazione

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

Risposta corretta: @JvmStatic annotazione

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

Costanti companion

Le proprietà pubbliche non const che sono costanti efficaci in un companion object devono essere annotate con @JvmField per essere esposte come campo statico.

Senza l'annotazione, queste proprietà sono disponibili solo come "getter" di istanze con nomi strani nel campo Companion statico. L'utilizzo di @JvmStatic anziché @JvmField sposta i "getter" con nomi strani nei metodi statici sulla classe, il che non è ancora corretto.

Risposta sbagliata: nessuna annotazione

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

Non corretto: @JvmStatic annotazione

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

Risposta corretta: @JvmField annotazione

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

Denominazione idiomatica

Kotlin ha convenzioni di chiamata diverse rispetto a Java, che possono cambiare il modo in cui assegni i nomi alle funzioni. Utilizza @JvmName per progettare nomi in modo che sembrino idiomatici secondo le convenzioni di entrambe le lingue o che corrispondano ai rispettivi nomi standard delle librerie.

Questo problema si verifica più spesso per le funzioni e le proprietà delle estensioni perché la posizione del tipo di destinatario è diversa.

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

Sovraccarico delle funzioni per i valori predefiniti

Le funzioni con parametri che hanno un valore predefinito devono utilizzare @JvmOverloads. Senza questa annotazione è impossibile richiamare la funzione utilizzando valori predefiniti.

Quando utilizzi @JvmOverloads, esamina i metodi generati per assicurarti che ciascuno abbia senso. In caso contrario, esegui uno o entrambi i seguenti refactoring fino a quando non soddisfi il requisito:

  • Modifica l'ordine dei parametri per scegliere quelli con valori predefiniti che si avvicinano alla fine.
  • Sposta i valori predefiniti in sovraccarichi di funzioni manuali.

Risposta sbagliata: no @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");
    }
}

Risposta corretta: @JvmOverloads annotazione.

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

Controlli lint

Requisiti

  • Versione di Android Studio: 3.2 Canary 10 o versioni successive
  • Versione del plug-in Android per Gradle: 3.2 o versioni successive

Controlli supportati

Ora ci sono controlli Lint di Android che ti aiuteranno a rilevare e segnalare alcuni dei problemi di interoperabilità descritti in precedenza. Al momento vengono rilevati solo i problemi in Java (per l'utilizzo di Kotlin). In particolare, i controlli supportati sono:

  • Nullo sconosciuto
  • Accesso alla proprietà
  • Nessuna parola chiave hard Kotlin
  • Ultimi parametri lambda

Android Studio

Per attivare questi controlli, vai a File > Preferenze > Editor > Ispezioni e seleziona le regole che vuoi attivare in Kotlin Interoperability:

Figura 1. Impostazioni di interoperabilità Kotlin in Android Studio.

Una volta selezionate le regole da attivare, i nuovi controlli verranno eseguiti quando esegui le ispezioni del codice (Analizza > Ispeziona codice...)

Build dalla riga di comando

Per abilitare questi controlli dalle build a riga di comando, aggiungi la seguente riga al file build.gradle:

Trendy

android {

    ...

    lintOptions {
        enable 'Interoperability'
    }
}

Kotlin

android {
    ...

    lintOptions {
        enable("Interoperability")
    }
}

Per l'insieme completo delle configurazioni supportate all'interno di lintOptions, consulta la documentazione di riferimento su Gradle DSL per Android.

Quindi, esegui ./gradlew lint dalla riga di comando.