Guía de interoperabilidad de Kotlin-Java

Este documento es un conjunto de reglas para crear API públicas en Java y Kotlin con la intención de que el código se sienta idiomático cuando se lo consuma desde el otro lenguaje.

Última actualización: 18 de mayo de 2018

Java (para consumo de Kotlin)

No uses palabras clave fijas

No utilices ninguna de las palabras clave fijas de Kotlin como nombre de métodos o campos, ya que requieren el uso de acentos graves como forma escapada cuando se las llama desde Kotlin. Se permiten palabras clave no fijas, palabras clave modificadoras e 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 de Any para los métodos o los nombres de las propiedades de extensión de Any para los campos, a menos que sea absolutamente necesario. Si bien los métodos y campos de miembros siempre tendrán prioridad sobre las funciones o propiedades de extensión de Any, puede ser difícil saber a cuál se llama cuando se lee el código.

Anotaciones de nulabilidad

Cada uno de los tipos de campo, valores que se muestran y parámetros no primitivos de una API pública debe tener una anotación de nulabilidad. Los tipos no anotados se interpretan como tipos de "plataforma", que tienen nulabilidad ambigua.

De forma predeterminada, las marcas del compilador de Kotlin respetan las anotaciones de JSR 305, pero lo hacen 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 RxJava 2’s Flowable.create() se define como:

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

Debido a que FlowableOnSubscribe es apto para la conversión de SAM, las llamadas de función de este método desde Kotlin se ven así:

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

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

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

Prefijos de propiedad

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

Los métodos de descriptores de acceso requieren un prefijo "get", mientras que los métodos que muestran valores booleanos pueden usar el prefijo "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 el 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 deseas que se expongan los métodos como propiedades, no uses prefijos no estándares como "has" o "set" ni descriptores de acceso sin prefijo "get". Los métodos con prefijos no estándares aún se pueden llamar como funciones que pueden ser aceptables según 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 (es decir, la sobrecarga del operador) en Kotlin. Asegúrate de que tenga sentido usar los nombres de los métodos 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 es atractiva y que filtra el lenguaje como un detalle de implementación.

Procura agregar @file:JvmMultifileClass para combinar los miembros de nivel superior de múltiples archivos en una única 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 de manera idiomática. Kotlin tiene varias opciones para definir esas interfaces, cada una con una leve diferencia.

Definición preferida

Las funciones de orden superior que están diseñadas para usarse desde Java no deberían tomar tipos de funciones que devuelvan Unit, ya que eso requeriría que los llamadores de Java devuelvan Unit.INSTANCE. En lugar de intercalar el tipo de función en la firma, usa interfaces funcionales (SAM). Además, considera usar interfaces funcionales (SAM) en lugar de interfaces normales cuando definas interfaces que se espera que se usen como lambdas, lo 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 deba 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. Si no se usa el prefijo fun en la interfaz, se fuerza al llamador a usar la sintaxis object : ..., lo 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 formato en Java. Los tipos sin formato se usan con poca frecuencia en Java y se deben evitar.

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 muestren colecciones de solo lectura compartidas o sin propietario de las API públicas, únelas en un contenedor que no se pueda modificar o realiza una copia defensiva. Kotlin aplica su propiedad de solo lectura, pero esa opción no está disponible en Java. Sin el wrapper o la copia defensiva, es posible vulnerar objetos invariantes mostrando una referencia de colección permanente.

Funciones complementarias

Para exponer como un método estático las funciones públicas de un objeto complementario, hay que anotarlas con @JvmStatic.

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

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

Para exponer como un campo estático las propiedades públicas no const que son constantes efectivas de un companion object, hay que anotarlas con @JvmField.

Sin la anotación, estas propiedades solo están disponibles como "métodos get" de instancias con nombres extraños en el campo Companion estático. Si usas @JvmStatic en lugar de @JvmField, los "métodos get" con nombres extraños se convertirán en métodos estáticos de la clase, lo que sigue siendo incorrecto.

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 a fin de diseñar nombres que se sientan idiomáticos para las convenciones de ambos lenguajes o para que coincidan con las nomenclaturas de las bibliotecas estándares correspondientes.

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

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 tenga sentido. De lo contrario, realiza una de las siguientes refactorizaciones, o ambas, hasta que estés 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 verificaciones de Android Lint que te ayudarán a detectar y marcar algunos de los problemas de interoperabilidad descritos con anterioridad. En este momento, solo se detectan problemas en Java (para consumo de Kotlin). 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 File > Preferences > Editor > Inspections y verifica las reglas que deseas habilitar en Kotlin Interoperability:

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

Una vez que hayas verificado las reglas que deseas habilitar, se ejecutarán las nuevas verificaciones cuando ejecutes 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.