Cómo llamar a código Kotlin desde Java

En este codelab, aprenderás cómo escribir o adaptar código Kotlin para que sea más fácil invocarlo desde código Java.

Qué aprenderás

  • Cómo usar @JvmField, @JvmStatic y otras anotaciones
  • Limitaciones para acceder a ciertas funciones del lenguaje Kotlin desde el código Java

Conocimientos que ya deberías tener

Este codelab está escrito para programadores y supone conocimientos básicos de Java y Kotlin.

En este codelab, se simula la migración de parte de un proyecto más grande escrito con el lenguaje de programación Java para incorporar código Kotlin nuevo.

Para simplificar las cosas, tendremos un solo archivo .java llamado UseCase.java, que representará la base de código existente.

Imaginemos que acabamos de reemplazar una funcionalidad escrita originalmente en Java por una versión nueva escrita en Kotlin, y debemos terminar de integrarla.

Importa el proyecto

El código del proyecto se puede clonar desde el proyecto de GitHub aquí: GitHub

Como alternativa, puedes descargar y extraer el proyecto de un archivo ZIP que se encuentra aquí:

Download Zip

Si usas IntelliJ IDEA, selecciona "Import Project".

Si usas Android Studio, selecciona "Import project (Gradle, Eclipse ADT, etc.)".

Abramos UseCase.java y comencemos a analizar los errores que vemos.

La primera función con un problema es registerGuest:

public static User registerGuest(String name) {
   User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
   Repository.addUser(guest);
   return guest;
}

Los errores para Repository.getNextGuestId() y Repository.addUser(...) son los mismos: "No se puede acceder a un elemento no estático desde un contexto estático".

Ahora, veamos uno de los archivos de Kotlin. Abre el archivo Repository.kt.

Vemos que nuestro repositorio es un singleton que se declara con la palabra clave object. El problema es que Kotlin genera una instancia estática dentro de nuestra clase, en lugar de exponerlas como propiedades y métodos estáticos.

Por ejemplo, se podría hacer referencia a Repository.getNextGuestId() con Repository.INSTANCE.getNextGuestId(), pero hay una mejor manera.

Podemos hacer que Kotlin genere métodos y propiedades estáticos si anotamos las propiedades y los métodos públicos del repositorio con @JvmStatic:

object Repository {
   val BACKUP_PATH = "/backup/user.repo"

   private val _users = mutableListOf<User>()
   private var _nextGuestId = 1000

   @JvmStatic
   val users: List<User>
       get() = _users

   @JvmStatic
   val nextGuestId
       get() = _nextGuestId++

   init {
       _users.add(User(100, "josh", "Joshua Calvert", listOf("admin", "staff", "sys")))
       _users.add(User(101, "dahybi", "Dahybi Yadev", listOf("staff", "nodes")))
       _users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
       _users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
   }

   @JvmStatic
   fun saveAs(path: String?):Boolean {
       val backupPath = path ?: return false

       val outputFile = File(backupPath)
       if (!outputFile.canWrite()) {
           throw FileNotFoundException("Could not write to file: $backupPath")
       }
       // Write data...
       return true
   }

   @JvmStatic
   fun addUser(user: User) {
       // Ensure the user isn't already in the collection.
       val existingUser = users.find { user.id == it.id }
       existingUser?.let { _users.remove(it) }
       // Add the user.
       _users.add(user)
   }
}

Agrega la anotación @JvmStatic a tu código con tu IDE.

Si volvemos a UseCase.java, las propiedades y los métodos en Repository ya no causan errores, excepto Repository.BACKUP_PATH. Volveremos a eso más adelante.

Por ahora, corrijamos el siguiente error en el método registerGuest().

Consideremos la siguiente situación: teníamos una clase StringUtils con varias funciones estáticas para operaciones de cadenas. Cuando lo convertimos a Kotlin, convertimos los métodos en funciones de extensión. Java no tiene funciones de extensión, por lo que Kotlin compila estos métodos como funciones estáticas.

Lamentablemente, si observamos el método registerGuest() dentro de UseCase.java, podemos ver que algo no está del todo bien:

User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);

El motivo es que Kotlin coloca estas funciones de "nivel superior" o de nivel de paquete dentro de una clase cuyo nombre se basa en el nombre del archivo. En este caso, como el archivo se llama StringUtils.kt, la clase correspondiente se llama StringUtilsKt.

Podríamos cambiar todas nuestras referencias de StringUtils a StringUtilsKt y corregir este error, pero esto no es lo ideal por los siguientes motivos:

  • Es posible que haya muchos lugares en nuestro código que deban actualizarse.
  • El nombre en sí es extraño.

Por lo tanto, en lugar de refactorizar nuestro código Java, actualicemos nuestro código Kotlin para usar un nombre diferente para estos métodos.

Abre StringUtils.Kt y busca la siguiente declaración de paquete:

package com.google.example.javafriendlykotlin

Podemos indicarle a Kotlin que use un nombre diferente para los métodos a nivel del paquete con la anotación @file:JvmName. Usemos esta anotación para nombrar la clase StringUtils.

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

Ahora, si volvemos a UseCase.java, podemos ver que se resolvió el error de StringUtils.nameToLogin().

Lamentablemente, este error se reemplazó por uno nuevo sobre los parámetros que se pasan al constructor de User. Continuemos con el siguiente paso y corrijamos este último error en UseCase.registerGuest().

Kotlin admite valores predeterminados para los parámetros. Podemos ver cómo se usan dentro del bloque init de Repository.kt.

Repository.kt:

_users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
_users.add(User(103, "warlow", groups = listOf("staff", "inactive")))

Podemos ver que, para el usuario "warlow", podemos omitir la inclusión de un valor para displayName porque hay un valor predeterminado especificado para él en User.kt.

User.kt:

data class User(
   val id: Int,
   val username: String,
   val displayName: String = username.toTitleCase(),
   val groups: List<String> = listOf("guest")
)

Lamentablemente, esto no funciona de la misma manera cuando se llama al método desde Java.

UseCase.java:

User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);

Los valores predeterminados no son compatibles con el lenguaje de programación Java. Para corregir esto, indiquemos a Kotlin que genere sobrecargas para nuestro constructor con la ayuda de la anotación @JvmOverloads.

Primero, debemos realizar una pequeña actualización en User.kt.

Dado que la clase User solo tiene un constructor principal y el constructor no incluye ninguna anotación, se omitió la palabra clave constructor. Sin embargo, ahora que queremos anotarlo, se debe incluir la palabra clave constructor:

data class User constructor(
    val id: Int,
    val username: String,
    val displayName: String = username.toTitleCase(),
    val groups: List<String> = listOf("guest")
)

Con la palabra clave constructor presente, podemos agregar la anotación @JvmOverloads:

data class User @JvmOverloads constructor(
    val id: Int,
    val username: String,
    val displayName: String = username.toTitleCase(),
    val groups: List<String> = listOf("guest")
)

Si volvemos a UseCase.java, podemos ver que ya no hay errores en la función registerGuest.

El siguiente paso es corregir la llamada interrumpida a user.hasSystemAccess() en UseCase.getSystemUsers(). Continúa con el siguiente paso para eso o sigue leyendo para profundizar en lo que hizo @JvmOverloads para corregir el error.

@JvmOverloads

Para comprender mejor lo que hace @JvmOverloads, creemos un método de prueba en UseCase.java:

private void testJvmOverloads() {
   User syrinx = new User(1001, "syrinx");
   User ione = new User(1002, "ione", "Ione Saldana");

   List<String> groups = new ArrayList<>();
   groups.add("staff");
   User beaulieu = new User(1002, "beaulieu", groups);
}

Podemos construir un User con solo dos parámetros, id y username:

User syrinx = new User(1001, "syrinx");

También podemos construir un User incluyendo un tercer parámetro para displayName y, al mismo tiempo, usar el valor predeterminado para groups:

User ione = new User(1002, "ione", "Ione Saldana");

Sin embargo, no es posible omitir displayName y solo proporcionar un valor para groups sin escribir código adicional:

Así que borremos esa línea o agreguemos “//” al principio para marcarla como comentario.

En Kotlin, si queremos combinar parámetros predeterminados y no predeterminados, debemos usar parámetros con nombre.

// This doesn't work...
User(104, "warlow", listOf("staff", "inactive"))
// But using named parameters, it does...
User(104, "warlow", groups = listOf("staff", "inactive"))

El motivo es que Kotlin generará sobrecargas para las funciones, incluidos los constructores, pero solo creará una sobrecarga por parámetro con un valor predeterminado.

Volvamos a UseCase.java y abordemos nuestro siguiente problema: la llamada a user.hasSystemAccess() en el método UseCase.getSystemUsers():

public static List<User> getSystemUsers() {
   ArrayList<User> systemUsers = new ArrayList<>();
   for (User user : Repository.getUsers()) {
       if (user.hasSystemAccess()) {     // Now has an error!
           systemUsers.add(user);
       }
   }
   return systemUsers;
}

Este es un error interesante. Si usas la función de autocompletado del IDE en la clase User, notarás que se cambió el nombre de hasSystemAccess() a getHasSystemAccess().

Para solucionar el problema, queremos que Kotlin genere un nombre diferente para la propiedad val hasSystemAccess. Para ello, podemos usar la anotación @JvmName. Volvamos a User.kt y veamos dónde deberíamos aplicarlo.

Existen dos formas de aplicar la anotación. La primera es aplicarla directamente al método get(), de la siguiente manera:

val hasSystemAccess
   @JvmName("hasSystemAccess")
   get() = "sys" in groups

Esto indica a Kotlin que cambie la firma del getter definido de forma explícita al nombre proporcionado.

Como alternativa, puedes aplicarlo a la propiedad con un prefijo get: de la siguiente manera:

@get:JvmName("hasSystemAccess")
val hasSystemAccess
   get() = "sys" in groups

El método alternativo es particularmente útil para las propiedades que usan un getter predeterminado definido de forma implícita. Por ejemplo:

@get:JvmName("isActive")
val active: Boolean

Esto permite cambiar el nombre del getter sin tener que definirlo de forma explícita.

A pesar de esta distinción, puedes usar la que te resulte más cómoda. Ambos harán que Kotlin cree un getter con el nombre hasSystemAccess().

Si volvemos a UseCase.java, podemos verificar que getSystemUsers() ahora no tiene errores.

El siguiente error se encuentra en formatUser(), pero, si deseas obtener más información sobre la convención de nomenclatura de los métodos get de Kotlin, sigue leyendo aquí antes de continuar con el siguiente paso.

Nombres de los métodos get y set

Cuando escribimos en Kotlin, es fácil olvidar que escribir código como el siguiente:

val myString = "Logged in as ${user.displayName}")

En realidad, llama a una función para obtener el valor de displayName. Para verificarlo, ve a Tools > Kotlin > Show Kotlin Bytecode en el menú y, luego, haz clic en el botón Decompile:

String myString = "Logged in as " + user.getDisplayName();

Cuando queremos acceder a ellos desde Java, debemos escribir explícitamente el nombre del getter.

En la mayoría de los casos, el nombre en Java de los métodos get para las propiedades de Kotlin es simplemente get + el nombre de la propiedad, como vimos con User.getHasSystemAccess() y User.getDisplayName(). La única excepción son las propiedades cuyos nombres comienzan con "is". En este caso, el nombre de Java para el getter es el nombre de la propiedad de Kotlin.

Por ejemplo, una propiedad en User como la siguiente:

val isAdmin get() = //...

Se accedería desde Java con lo siguiente:

boolean userIsAnAdmin = user.isAdmin();

Cuando se usa la anotación @JvmName, Kotlin genera código de bytes que tiene el nombre especificado, en lugar del predeterminado, para el elemento que se anota.

Esto funciona de la misma manera para los métodos setter, cuyos nombres generados siempre son set + nombre de la propiedad. Por ejemplo, considera la siguiente clase:

class Color {
   var red = 0f
   var green = 0f
   var blue = 0f
}

Imaginemos que queremos cambiar el nombre del setter de setRed() a updateRed(), pero dejar los getters como están. Podríamos usar la versión @set:JvmName para hacer esto:

class Color {
   @set:JvmName("updateRed")
   var red = 0f
   @set:JvmName("updateGreen")
   var green = 0f
   @set:JvmName("updateBlue")
   var blue = 0f
}

Desde Java, podríamos escribir lo siguiente:

color.updateRed(0.8f);

UseCase.formatUser() usa el acceso directo a los campos para obtener los valores de las propiedades de un objeto User.

En Kotlin, las propiedades se suelen exponer a través de métodos get y set. Esto incluye las propiedades de val.

Es posible cambiar este comportamiento con la anotación @JvmField. Cuando se aplica a una propiedad en una clase, Kotlin omitirá la generación de métodos get (y set para las propiedades var), y se podrá acceder directamente al campo de respaldo.

Dado que los objetos User son inmutables, nos gustaría exponer cada una de sus propiedades como campos, por lo que anotaremos cada uno de ellos con @JvmField:

data class User @JvmOverloads constructor(
   @JvmField val id: Int,
   @JvmField val username: String,
   @JvmField val displayName: String = username.toTitleCase(),
   @JvmField val groups: List<String> = listOf("guest")
) {
   @get:JvmName("hasSystemAccess")
   val hasSystemAccess
       get() = "sys" in groups
}

Si volvemos a UseCase.formatUser(), ahora podemos ver que se corrigieron los errores.

@JvmField o const

Con eso, hay otro error de aspecto similar en el archivo UseCase.java:

Repository.saveAs(Repository.BACKUP_PATH);

Si usamos la función de autocompletar aquí, podemos ver que hay un Repository.getBACKUP_PATH(), por lo que puede ser tentador cambiar la anotación en BACKUP_PATH de @JvmStatic a @JvmField.

Vamos a intentarlo. Vuelve a Repository.kt y actualiza la anotación:

object Repository {
   @JvmField
   val BACKUP_PATH = "/backup/user.repo"

Si ahora miramos UseCase.java, veremos que el error desapareció, pero también hay una nota en BACKUP_PATH:

En Kotlin, los únicos tipos que pueden ser const son los primitivos, como int, float y String. En este caso, dado que BACKUP_PATH es una cadena, podemos obtener un mejor rendimiento si usamos const val en lugar de un val anotado con @JvmField, y conservamos la capacidad de acceder al valor como un campo.

Cambiemos eso ahora en Repository.kt:

object Repository {
   const val BACKUP_PATH = "/backup/user.repo"

Si volvemos a UseCase.java, podemos ver que solo queda un error.

El error final dice Exception: 'java.io.IOException' is never thrown in the corresponding try block..

Si observamos el código de Repository.saveAs en Repository.kt, vemos que sí genera una excepción. ¿Qué sucede?

Java tiene el concepto de "excepción verificada". Estas son excepciones de las que se podría recuperar, como que el usuario escriba mal un nombre de archivo o que la red no esté disponible temporalmente. Después de que se detecta una excepción verificada, el desarrollador puede proporcionar comentarios al usuario sobre cómo solucionar el problema.

Dado que las excepciones verificadas se verifican en el tiempo de compilación, debes declararlas en la firma del método:

public void openFile(File file) throws FileNotFoundException {
   // ...
}

Por otro lado, Kotlin no tiene excepciones verificadas, y eso es lo que causa el problema aquí.

La solución es pedirle a Kotlin que agregue el IOException que se podría arrojar a la firma de Repository.saveAs(), de modo que el código de bytes de JVM lo incluya como una excepción verificada.

Esto lo hacemos con la anotación @Throws de Kotlin, que ayuda con la interoperabilidad de Java/Kotlin. En Kotlin, las excepciones se comportan de manera similar a Java, pero, a diferencia de Java, Kotlin solo tiene excepciones no verificadas. Por lo tanto, si quieres informar a tu código Java que una función de Kotlin arroja una excepción, debes usar la anotación @Throws en la firma de la función de Kotlin. Cambia a Repository.kt file y actualiza saveAs() para incluir la nueva anotación:

@JvmStatic
@Throws(IOException::class)
fun saveAs(path: String?) {
   val outputFile = File(path)
   if (!outputFile.canWrite()) {
       throw FileNotFoundException("Could not write to file: $path")
   }
   // Write data...
}

Con la anotación @Throws, podemos ver que se corrigieron todos los errores del compilador en UseCase.java. ¡Hip, hip, hurra!

Quizás te preguntes si ahora tendrás que usar bloques try y catch cuando llames a saveAs() desde Kotlin.

No. Recuerda que Kotlin no tiene excepciones verificadas y que agregar @Throws a un método no cambia eso:

fun saveFromKotlin(path: String) {
   Repository.saveAs(path)
}

Aun así, es útil capturar excepciones cuando se pueden controlar, pero Kotlin no te obliga a hacerlo.

En este codelab, abarcamos los conceptos básicos para escribir código Kotlin que también admita la escritura de código Java idiomático.

Hablamos sobre cómo podemos usar anotaciones para cambiar la forma en que Kotlin genera su código de bytes de JVM, por ejemplo:

  • @JvmStatic para generar métodos y miembros estáticos.
  • @JvmOverloads para generar métodos sobrecargados para las funciones que tienen valores predeterminados.
  • @JvmName para cambiar el nombre de los métodos get y set
  • @JvmField para exponer una propiedad directamente como un campo, en lugar de hacerlo a través de métodos get y set
  • @Throws para declarar excepciones verificadas.

El contenido final de nuestros archivos es el siguiente:

User.kt

data class User @JvmOverloads constructor(
   @JvmField val id: Int,
   @JvmField val username: String,
   @JvmField val displayName: String = username.toTitleCase(),
   @JvmField val groups: List<String> = listOf("guest")
) {
   val hasSystemAccess
       @JvmName("hasSystemAccess")
       get() = "sys" in groups
}

Repository.kt

object Repository {
   const val BACKUP_PATH = "/backup/user.repo"

   private val _users = mutableListOf<User>()
   private var _nextGuestId = 1000

   @JvmStatic
   val users: List<User>
       get() = _users

   @JvmStatic
   val nextGuestId
       get() = _nextGuestId++

   init {
       _users.add(User(100, "josh", "Joshua Calvert", listOf("admin", "staff", "sys")))
       _users.add(User(101, "dahybi", "Dahybi Yadev", listOf("staff", "nodes")))
       _users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
       _users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
   }

   @JvmStatic
   @Throws(IOException::class)
   fun saveAs(path: String?):Boolean {
       val backupPath = path ?: return false

       val outputFile = File(backupPath)
       if (!outputFile.canWrite()) {
           throw FileNotFoundException("Could not write to file: $backupPath")
       }
       // Write data...
       return true
   }

   @JvmStatic
   fun addUser(user: User) {
       // Ensure the user isn't already in the collection.
       val existingUser = users.find { user.id == it.id }
       existingUser?.let { _users.remove(it) }
       // Add the user.
       _users.add(user)
   }
}

StringUtils.kt

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

fun String.toTitleCase(): String {
   if (isNullOrBlank()) {
       return this
   }

   return split(" ").map { word ->
       word.foldIndexed("") { index, working, char ->
           val nextChar = if (index == 0) char.toUpperCase() else char.toLowerCase()
           "$working$nextChar"
       }
   }.reduceIndexed { index, working, word ->
       if (index > 0) "$working $word" else word
   }
}

fun String.nameToLogin(): String {
   if (isNullOrBlank()) {
       return this
   }
   var working = ""
   toCharArray().forEach { char ->
       if (char.isLetterOrDigit()) {
           working += char.toLowerCase()
       } else if (char.isWhitespace() and !working.endsWith(".")) {
           working += "."
       }
   }
   return working
}