Guía de interoperabilidad de Kotlin-Java

Este documento es un conjunto de reglas para la creación de APIs públicas en Java y Kotlin con la intención de que el código se sienta idiomático cuando se consuma del otro idioma.

Última actualización: 29 de julio de 2024

Java (para consumo de Kotlin)

No uses palabras clave fijas

No uses ninguna de las palabras clave fijas de Kotlin como nombre de los métodos. o campos. Estas requieren el uso de acentos graves como escape cuando se llame desde Kotlin Palabras clave no definitivas, palabras clave modificadoras y se permiten identificadores especiales.

Por ejemplo, la función when de Mockito requiere un acento grave cuando se usa desde Kotlin:

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

Evita usar nombres de extensión Any

Evita usar los nombres de las funciones de extensión en Any para o los nombres de las propiedades de extensión en Any para a menos que sea absolutamente necesario. Si bien los métodos y campos de miembros siempre tienen prioridad sobre las funciones o propiedades de extensión de Any, puede difícil cuando se lee el código para saber a cuál se llama.

Anotaciones de nulabilidad

Todos los tipos de campo, retornos y parámetros no primitivos en una API pública deben tienen una anotación de nulabilidad. Los tipos no anotados se interpretan como “plataforma” tipos, que tienen nulabilidad ambigua.

De forma predeterminada, las marcas del compilador de Kotlin respetan las anotaciones de JSR 305, pero las marcan. con advertencias. También puedes configurar una marca para que el compilador trate las anotaciones como errores.

Los parámetros lambda van al final

Los tipos de parámetros aptos para la conversión de SAM deben ser los últimos.

Por ejemplo, la firma del método Flowable.create() de RxJava 2 se define de la siguiente manera:

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

Como FlowableOnSuscríbete es apto para la conversión de SAM, las llamadas a función de este método de Kotlin se verá de la siguiente manera:

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

Sin embargo, si los parámetros se revirtieron en la firma del método, las llamadas a funciones podrías usar la sintaxis end-lambda:

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

Prefijos de propiedad

Para que un método se represente como una propiedad en Kotlin, usa el estilo estricto "bean" y se debe usar un prefijo.

Los métodos de acceso requieren un prefijo get o, para los métodos que muestran valores booleanos, 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()

Los métodos de mutador asociados requieren un prefijo 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 quieres que los métodos se expongan como propiedades, no uses prefijos no estándares, como has, set o descriptores de acceso sin el prefijo get. Métodos con prefijos no estándar aún se pueden llamar como funciones, las cuales pueden ser aceptables según el el comportamiento del método.

Sobrecarga del operador

Ten en cuenta los nombres de métodos que permiten una sintaxis especial del sitio de llamada (como sobrecarga del operador en Kotlin). Asegúrate de que los nombres de los métodos tiene sentido usarlos con la sintaxis abreviada.

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 (para consumo de Java)

Nombre del archivo

Cuando un archivo contenga funciones o propiedades de nivel superior, siempre anótalo. con @file:JvmName("Foo") para proporcionar un buen nombre.

De forma predeterminada, los miembros de nivel superior de un archivo MyClass.kt terminarán en una clase llamada MyClassKt, que no resulta atractivo y filtra el lenguaje como una implementación en detalle.

Te recomendamos agregar @file:JvmMultifileClass para combinar los miembros de nivel superior de varios archivos en una sola clase.

Argumentos lambda

Las interfaces de método único (SAM) definidas en Java se pueden implementar en Kotlin y Java con la sintaxis lambda, que intercala la implementación en un lenguaje de una nueva manera. Kotlin tiene varias opciones para definir esas interfaces, cada una con una leve la diferencia.

Definición preferida

Funciones de orden superior diseñadas para usarse desde Java no debería tomar los tipos de funciones que devuelvan Unit como lo harían Requerir que los llamadores de Java devuelvan Unit.INSTANCE. En lugar de intercalar la función escribe la firma, usa interfaces funcionales (SAM). También considera usar interfaces funcionales (SAM) en lugar de cuando se definen interfaces que se espera que se usen como lambdas, que permite el uso idiomático de Kotlin.

Considera esta definición de Kotlin:

fun interface GreeterCallback {
  fun greetName(String name)
}

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

Cuando se invoca desde Kotlin:

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

Cuando se invoca desde Java:

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

Incluso cuando el tipo de función no devuelve un Unit, podría ser una buena idea convertirla en una interfaz con nombre para permitir que los llamadores la implementen con una clase con nombre y no solo con lambdas (en ambos casos, y Java).

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

Evita los tipos de funciones que devuelven Unit

Considera esta definición de Kotlin:

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

Requiere que los llamadores de Java devuelvan Unit.INSTANCE:

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

Evite las interfaces funcionales cuando la implementación deba tener estado

Cuando la implementación de la interfaz debe tener un estado, el uso de la sintaxis lambda no tiene sentido. Comparable es un ejemplo destacado, ya que está diseñado para comparar this con other, y las lambdas no tienen this. No Si se agrega el prefijo fun a la interfaz, se fuerza al llamador a usar object : .... de texto, que le permite tener un estado y le proporciona una sugerencia al llamador.

Considera esta definición de Kotlin:

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

Evita la sintaxis lambda en Kotlin, que requiere esta versión más larga:

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

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

Evita usar parámetros Nothing genéricos

Un tipo cuyo parámetro genérico es Nothing se expone como tipos sin procesar en Java. Sin procesar tipos de bloques se usan rara vez en Java y deberían evitarse.

Excepciones de documentos

Las funciones que pueden arrojar excepciones verificadas deben documentarse con @Throws. Las excepciones de tiempo de ejecución deben estar documentadas en KDoc.

Ten en cuenta la función que se delega a las API, ya que pueden arrojar excepciones verificadas que Kotlin, de otra forma, permite propagar de forma silenciosa.

Copias defensivas

Cuando se devuelven colecciones de solo lectura compartidas o sin propietario de APIs públicas, unir en un contenedor que no se puede modificar o hacer una copia defensiva. A pesar de Kotlin aplicación de su propiedad de solo lectura, no existe tal aplicación en Java lado derecho. Sin el wrapper o la copia defensiva, los invariantes pueden ser violados por mostrar una referencia de colección de larga duración.

Funciones complementarias

Las funciones públicas de un objeto complementario deben tener anotaciones con @JvmStatic se expongan como un método estático.

Sin la anotación, estas funciones solo están disponibles como métodos de instancia en un campo Companion estático.

Incorrecto: Sin anotación

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

Correcto: Anotación @JvmStatic

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

Constantes complementarias

Las propiedades públicas no const que son constantes efectivas de un companion object deben anotarse con @JvmField para exponerlas como un campo estático.

Sin la anotación, estas propiedades solo están disponibles como nombres extraños "método get" de la instancia en el campo estático Companion. Se está usando @JvmStatic en su lugar de @JvmField mueve a los "métodos get" con nombres extraños. a métodos estáticos en la clase, que aún es incorrecta.

Incorrecto: Sin anotación

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

Incorrecto: Anotación @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());
    }
}

Correcto: Anotación @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);
    }
}

Nomenclatura idiomática

Kotlin tiene diferentes convenciones de llamadas respecto de Java que pueden cambiar la forma en que nombras funciones. Usa @JvmName para diseñar nombres que se sientan idiomáticos para las convenciones de ambos lenguajes o para que coincida con su respectiva biblioteca estándar de nombres.

Esto ocurre con mayor frecuencia para las funciones y propiedades de extensión porque la ubicación del tipo de receptor es diferente.

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

Sobrecargas de funciones predeterminadas

Las funciones con parámetros que tienen un valor predeterminado deben usar @JvmOverloads. Sin esta anotación, es imposible invocar la función con cualquier valor predeterminado.

Cuando uses @JvmOverloads, inspecciona los métodos generados para asegurarte de que cada uno tienen sentido. De lo contrario, realiza una o ambas de las siguientes refactorizaciones hasta que esté satisfecho:

  • Cambia el orden de los parámetros para que los que tengan valores predeterminados se dirijan hacia el final.
  • Mueve los valores predeterminados a sobrecargas de funciones manuales.

Incorrecto: Sin @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");
    }
}

Correcto: Anotación @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");
    }
}

Verificaciones de Lint

Requisitos

  • Versión de Android Studio: 3.2 Canary 10 o posteriores
  • Versión del complemento de Gradle para Android: 3.2 o posteriores

Verificaciones compatibles

Ahora hay comprobaciones de Android Lint que te ayudarán a detectar y marcar algunos de los problemas de interoperabilidad descritos anteriormente. Solo problemas en Java (para Kotlin consumo). Específicamente, las verificaciones compatibles son las siguientes:

  • Nulabilidad desconocida
  • Acceso a la propiedad
  • No usar palabras clave fijas de Kotlin
  • Los parámetros lambda van al final

Android Studio

Para habilitar estas verificaciones, ve a Archivo > Preferencias > Editor > Inspecciones y Verifica las reglas que deseas habilitar en la Interoperabilidad de Kotlin:

Figura 1: Configuración de interoperabilidad de Kotlin en Android Studio

Una vez que hayas marcado las reglas que deseas habilitar, las nuevas comprobaciones cuando ejecutas las inspecciones de tu código (Analyze > Inspect Code...)

Compilaciones de líneas de comandos

Para habilitar estas verificaciones desde las compilaciones de líneas de comandos, agrega la siguiente línea en Tu archivo build.gradle:

Groovy

android {

    ...

    lintOptions {
        enable 'Interoperability'
    }
}

Kotlin

android {
    ...

    lintOptions {
        enable("Interoperability")
    }
}

Para conocer el conjunto completo de configuraciones admitidas dentro de lintOptions, consulta la referencia Gradle DSL de Android.

A continuación, ejecuta ./gradlew lint desde la línea de comandos.