Guida all'interoperabilità di Kotlin-Java

Questo documento è un insieme di regole per la creazione di API pubbliche in Java e Kotlin con l'intento di far sì che il codice risulti idiomatico se utilizzato dall'altro lingua.

Ultimo aggiornamento: 29-07-2024

Java (per l'utilizzo di Kotlin)

Nessuna parola chiave difficile

Non utilizzare nessuna delle parole chiave hard di Kotlin come nome dei metodi o campi. Richiedono l'uso di accenti inversi come escape durante la chiamata da Kotlin. Parole chiave flessibili, parole chiave modificatori e sono consentiti identificatori speciali.

Ad esempio, la funzione when di Mockito richiede l'accento grave quando viene utilizzata da Kotlin:

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

Evita i nomi delle estensioni Any

Evita di utilizzare i nomi delle funzioni di estensione su Any per o i nomi delle proprietà dell'estensione su Any per campi, a meno che non siano assolutamente necessari. Sebbene i metodi e i campi dei membri hanno la precedenza sulle funzioni o sulle proprietà delle estensioni di Any, possono essere difficile capire quale viene chiamato durante la lettura del codice.

Annotazioni di nullità

In un'API pubblica, ogni parametro, ritorno e tipo di campo non primitivo deve dispongono di un'annotazione con valore nullo. I tipi non annotati sono interpretati come "piattaforma" type, che hanno un valore nullo ambiguo.

Per impostazione predefinita, i flag del compilatore Kotlin rispettano le annotazioni JSR 305, ma le contrassegnano con gli avvisi. Puoi anche impostare un flag in modo che il compilatore gestisca 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 Flowable.create() di RxJava 2 è definita come:

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

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

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

Se i parametri sono stati invertiti nella firma del metodo, però, le chiamate di funzione potresti usare la sintassi trailing-lambda:

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

Prefissi proprietà

Affinché un metodo venga rappresentato come proprietà in Kotlin, utilizza rigoroso stile "a fagiolo" è necessario utilizzare un prefisso.

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

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 esposti come proprietà, non utilizzare prefissi non standard come Funzioni di accesso con prefisso has, set o senza get. Metodi con prefissi non standard sono comunque richiamabili come funzioni, il che può essere accettabile a seconda comportamento del metodo.

Sovraccarico dell'operatore

Presta attenzione ai nomi dei metodi che consentono una sintassi speciale per i siti di chiamata (come sovraccarichi dell'operatore in Kotlin). Assicurati che i metodi denominano come ha senso usare con la 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 il consumo Java)

Nome del file

Quando un file contiene funzioni o proprietà di primo livello, annotalo sempre con @file:JvmName("Foo") per dare un bel nome.

Per impostazione predefinita, i membri di primo livello in un file MyClass.kt finiranno in una classe chiamata MyClassKt, che è sgradevole e fa trapelare il linguaggio utilizzato come implementazione dettaglio.

Valuta la possibilità di aggiungere @file:JvmMultifileClass per combinare i membri di primo livello di più file in un'unica classe.

Argomenti Lambda

Le interfacce SAM definite in Java possono essere implementate sia in Kotlin Java usando la sintassi lambda, che allinea l'implementazione in un modo in molti modi diversi. Kotlin ha diverse opzioni per definire queste interfacce, ognuna con una la differenza.

Definizione preferita

Funzioni di ordine superiore destinate all'utilizzo da Java non deve accettare tipi di funzione che restituiscono Unit come faresti richiede che i chiamanti Java restituiscano Unit.INSTANCE. Invece di incorporare la funzione digita la firma, utilizza le interfacce funzionali (SAM). Inoltre Prendi in considerazione l'utilizzo di interfacce funzionali (SAM) invece delle normali quando si definiscono le interfacce che dovrebbero essere usate come lambda, che consente l'uso idiomatico da parte di Kotlin.

Considera questa definizione di Kotlin:

fun interface GreeterCallback {
  fun greetName(String name)
}

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

Quando la chiamata viene richiamata da Kotlin:

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

Se la richiesta viene richiamata da Java:

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

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

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

Evita i tipi di funzioni 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 interfacce funzionali se l'implementazione deve avere uno stato

Quando l'implementazione dell'interfaccia deve avere uno stato, utilizzando la funzione lambda la sintassi è senza senso. Comparable è un esempio importante, perché l'obiettivo è confrontare this con other e i lambda non hanno this. No l'aggiunta di fun all'interfaccia impone al chiamante di utilizzare object : ... che gli consente di avere uno stato e di fornire un suggerimento al chiamante.

Considera questa definizione di Kotlin:

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

Impedisce la sintassi lambda in Kotlin e richiede questa versione più lunga:

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

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

Evita Nothing di caratteri generici

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

Eccezioni relative ai documenti

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

Presta attenzione alle API a cui delega una funzione, che potrebbero restituire eccezioni che Kotlin consente altrimenti di propagare silenziosamente.

Copie difensive

Quando restituisci raccolte di sola lettura condivise o non possedute da API pubbliche, esegui il wrapping in un container non modificabile o eseguire una copia difensiva. Nonostante Kotlin l'applicazione forzata della proprietà di sola lettura, non è prevista lato server. Senza il wrapper o il testo difensivo, le invarianti possono essere violate che restituisce un riferimento a una raccolta di lunga durata.

Funzioni companion

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

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

Risposta errata: nessuna annotazione

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

Corretto: @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 effettive in un companion object devono essere annotate con @JvmField per essere esposte come campo statico.

Senza l'annotazione, queste proprietà sono disponibili solo con nomi strani istanza "getters" nel campo Companion statico. Utilizzo di @JvmStatic di @JvmField muove i "getter" con nomi strani a metodi statici sulla classe, che è ancora errato.

Risposta errata: 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: annotazione @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());
    }
}

Corretto: @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 le funzioni per i nomi. Usa @JvmName per creare nomi in modo che risultino idiomatici per entrambe le convenzioni della lingua o per le rispettive librerie standard di denominazione.

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

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

Sovraccarico delle funzioni per i valori predefiniti

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

Quando utilizzi @JvmOverloads, ispeziona i metodi generati per assicurarti che ciascuno ha senso. In caso contrario, esegui uno o entrambi i seguenti refactoring fino al completamento:

  • Modifica l'ordine dei parametri in modo da preferire quelli con valori predefiniti corrispondenti a fine.
  • Sposta i valori predefiniti in sovraccarichi di funzioni manuali.

Risposta errata: 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 esatta: annotazione @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");
    }
}

Controllo pelucchi

Requisiti

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

Assegni supportati

Sono ora disponibili controlli Android Lint che ti aiuteranno a rilevare e segnalare alcuni dei sui problemi di interoperabilità descritti in precedenza. Solo problemi in Java (per Kotlin il consumo energetico). In particolare, i controlli supportati sono:

  • Nullità sconosciuta
  • Accesso alla proprietà
  • Nessuna parola chiave hard Kotlin
  • Ultimo parametro Lambda

Android Studio

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

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

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

Build dalla riga di comando

Per abilitare questi controlli dalle build della riga di comando, aggiungi la riga seguente in il tuo file build.gradle:

Alla moda

android {

    ...

    lintOptions {
        enable 'Interoperability'
    }
}

Kotlin

android {
    ...

    lintOptions {
        enable("Interoperability")
    }
}

Per l'insieme completo delle configurazioni supportate all'interno di lintOptions, fai riferimento alle Riferimento DSL per Android Gradle.

Poi esegui ./gradlew lint dalla riga di comando.