Como chamar código Kotlin em Java

Neste codelab, você aprenderá a programar ou adaptar seu código Kotlin para facilitar a chamada dele no código Java.

O que você aprenderá

  • Como usar o @JvmField, o @JvmStatic e outras anotações.
  • Limitações para acessar determinados recursos da linguagem Kotlin no código Java

O que você já precisa saber

Este codelab foi escrito para programadores e requer conhecimento básico de Java e Kotlin.

Este codelab simula a migração de parte de um projeto maior escrito com a linguagem de programação Java para incorporar um novo código Kotlin.

Para simplificar, teremos um único arquivo .java chamado UseCase.java, que representará a base do código existente.

Vamos imaginar que substituímos algumas funcionalidades criadas originalmente em Java por uma nova versão escrita em Kotlin, e precisamos finalizar a integração.

Importar o projeto

Você pode clonar o código do projeto neste projeto: GitHub

Se preferir, faça o download e extraia o projeto em um arquivo ZIP encontrado aqui:

Fazer o download do ZIP

Se você estiver usando o IntelliJ IDEA, selecione "Import Project".

Se você está usando o Android Studio, selecione "Import project (Gradle, Eclipse ADT, etc.)".

Vamos abrir o UseCase.java e começar a trabalhar nos erros que vemos.

A primeira função com um problema é registerGuest:

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

Os erros para Repository.getNextGuestId() e Repository.addUser(...) são os mesmos: "não estático pode ser acessado a partir de um contexto estático."

Agora, vamos analisar um dos arquivos Kotlin. Abra o arquivo Repository.kt.

Notamos que nosso repositório é um Singleton declarado usando a palavra-chave de objeto. O problema é que o Kotlin está gerando uma instância estática dentro da nossa classe, em vez de expô-las como propriedades e métodos estáticos.

Por exemplo, Repository.getNextGuestId() pode ser referenciado usando Repository.INSTANCE.getNextGuestId(), mas há uma maneira melhor.

Podemos fazer com que o Kotlin gere métodos e propriedades estáticas anotando as propriedades e os métodos públicos do repositório com @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)
   }
}

Adicione a anotação @JvmStatic ao código usando o ambiente de desenvolvimento integrado.

Se voltarmos para UseCase.java, as propriedades e os métodos em Repository não causarão mais erros, exceto Repository.BACKUP_PATH. Voltaremos a isso mais tarde.

Por enquanto, vamos corrigir o próximo erro no método registerGuest().

Considere o seguinte cenário: tínhamos uma classe StringUtils com várias funções estáticas para operações de string. Quando convertemos para Kotlin, convertemos os métodos em funções de extensão. O Java não tem funções de extensão. Por isso, o Kotlin compila esses métodos como funções estáticas.

Se observarmos o método registerGuest() dentro de UseCase.java, veremos que algo não está certo:

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

O motivo é que o Kotlin coloca essas funções de nível superior em uma classe cujo nome é baseado no nome do arquivo. Nesse caso, como o nome do arquivo é StringUtils.kt, a classe correspondente é chamada de StringUtilsKt.

Poderíamos mudar todas as nossas referências de StringUtils para StringUtilsKt e corrigir esse erro, mas isso não é o ideal porque:

  • Talvez haja muitos lugares no código que precisam ser atualizados.
  • O nome em si é estranho.

Em vez de refatorar o código do Java, vamos atualizar o código em Kotlin para usar um nome diferente para esses métodos.

Abra StringUtils.Kt e localize a seguinte declaração de pacote:

package com.google.example.javafriendlykotlin

Podemos instruir o Kotlin a usar um nome diferente para os métodos no nível do pacote com a anotação @file:JvmName. Vamos usar essa anotação para nomear a classe StringUtils.

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

Agora, ao observar UseCase.java, vemos que o erro para StringUtils.nameToLogin() foi resolvido.

Esse erro foi substituído por um novo sobre os parâmetros que estão sendo transmitidos ao construtor do User. Vamos para a próxima etapa e corrigir esse último erro no UseCase.registerGuest().

O Kotlin é compatível com valores padrão para parâmetros. Para ver como eles são usados, consulte o bloco init do Repository.kt.

Repository.kt:

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

Vemos que o usuário "warlow" pode ignorar a inserção de um valor para displayName porque há um valor padrão especificado para ele em User.kt.

User.kt:

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

Infelizmente, isso não funciona da mesma forma ao chamar o método do Java.

UseCase.java:

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

Os valores padrão não são compatíveis com a linguagem de programação Java. Para corrigir isso, vamos pedir ao Kotlin para gerar sobrecargas para nosso construtor com a ajuda da anotação @JvmOverloads.

Primeiro, temos que fazer uma pequena atualização em User.kt.

Como a classe User só tem um único construtor principal e o construtor não inclui nenhuma anotação, a palavra-chave constructor foi omitida. Agora que queremos anotá-la, a palavra-chave constructor precisa ser incluída:

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

Com a palavra-chave constructor presente, podemos adicionar a anotação @JvmOverloads:

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

Se voltarmos para UseCase.java, poderemos ver que não há mais erros na função registerGuest.

A próxima etapa é corrigir a chamada interrompida para user.hasSystemAccess() em UseCase.getSystemUsers(). Continue para a próxima etapa ou continue lendo para saber mais detalhes sobre o que @JvmOverloads fez para corrigir o erro.

@JvmOverloads

Para entender melhor o que @JvmOverloads faz, vamos criar um método de teste em 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 um User com apenas dois parâmetros, id e username:

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

Também podemos construir um User incluindo um terceiro parâmetro para displayName e, ao mesmo tempo, usar o valor padrão para groups:

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

No entanto, não é possível pular displayName e apenas fornecer um valor para groups sem escrever mais código:

Então exclua a linha ou preceda-a com "//' para comentar".

No Kotlin, se quisermos combinar parâmetros padrão e não padrão, será necessário usar parâmetros nomeados.

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

O motivo é que o Kotlin gerará sobrecargas para funções, incluindo construtores, mas criará apenas uma sobrecarga por parâmetro com um valor padrão.

Vamos analisar UseCase.java e resolver nosso próximo problema: a chamada para user.hasSystemAccess() no 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;
}

Esse erro é interessante. Se você usar o recurso de preenchimento automático do seu ambiente de desenvolvimento integrado na classe User, perceberá que hasSystemAccess() foi renomeado como getHasSystemAccess().

Para corrigir o problema, queremos que o Kotlin gere um nome diferente para a propriedade val hasSystemAccess. Para fazer isso, podemos usar a anotação @JvmName. Volte para User.kt e veja onde aplicar.

Há duas maneiras de aplicar a anotação. A primeira é aplicá-la diretamente ao método get(), desta forma:

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

Isso sinaliza ao Kotlin para mudar a assinatura do getter definido explicitamente para o nome fornecido.

Como alternativa, é possível aplicá-la à propriedade usando um prefixo get: como este:

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

O método alternativo é particularmente útil para propriedades que usam um getter padrão e definido implicitamente. Exemplo:

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

Isso permite que o nome do getter seja alterado sem ter que definir explicitamente um getter.

Apesar dessa distinção, você pode usar a que for melhor para você. Ambos farão com que o Kotlin crie um getter com o nome hasSystemAccess().

Se voltarmos para o UseCase.java, verificaremos se o getSystemUsers() não apresenta erros.

O próximo erro é em formatUser(), mas se você quiser ler mais sobre a convenção de nomenclatura do getter do Kotlin, continue lendo aqui antes de avançar para a próxima etapa.

Nomeação de getter e setter

Quando estamos escrevendo Kotlin, é fácil esquecer esse código, como:

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

Na verdade, está chamando uma função para receber o valor de displayName. Para verificar isso, acesse Tools > Kotlin > Show Kotlin Bytecode no menu e clique no botão Descompile:

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

Para acessar essas APIs no Java, precisamos escrever o nome do getter explicitamente.

Na maioria dos casos, o nome Java de getters para propriedades Kotlin é simplesmente get + o nome da propriedade, como vimos com User.getHasSystemAccess() e User.getDisplayName(). A única exceção são as propriedades com nomes que começam com "is". Nesse caso, o nome Java para o getter é o nome da propriedade Kotlin.

Por exemplo, uma propriedade em User, como:

val isAdmin get() = //...

Seria acessado em Java com:

boolean userIsAnAdmin = user.isAdmin();

Ao usar a anotação @JvmName, o Kotlin gera um bytecode que tem o nome especificado, em vez do padrão, para o item que está sendo anotado.

Isso funciona da mesma forma para setters, cujos nomes gerados são sempre set + nome da propriedade. Por exemplo, veja a classe a seguir:

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

Vamos imaginar que queremos alterar o nome do setter de setRed() para updateRed(), deixando os getters sozinhos. Podemos usar a versão @set:JvmName para fazer isso:

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

A partir de Java, seria possível escrever:

color.updateRed(0.8f);

UseCase.formatUser() usa o acesso direto ao campo para receber os valores das propriedades de um objeto User.

No Kotlin, as propriedades normalmente são expostas por meio de getters e setters. Isso inclui propriedades val.

É possível mudar esse comportamento usando a anotação @JvmField. Quando isso for aplicado a uma propriedade em uma classe, o Kotlin pulará a geração de métodos getter (e setter para propriedades var), e o campo de apoio poderá ser acessado diretamente.

Como os objetos User são imutáveis, queremos expor cada uma delas como campos. Por isso, anotaremos cada um deles com @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
}

Se observarmos o UseCase.formatUser() agora, veremos que os erros foram corrigidos.

@JvmField ou const

Com isso, há outro erro semelhante no arquivo UseCase.java:

Repository.saveAs(Repository.BACKUP_PATH);

Se usarmos o preenchimento automático aqui, podemos ver que há um Repository.getBACKUP_PATH(), então pode ser tentador mudar a anotação em BACKUP_PATH de @JvmStatic para @JvmField.

Vamos tentar fazer isso. Volte para o Repository.kt e atualize a anotação:

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

Se observarmos UseCase.java agora, veremos que o erro desapareceu, mas também há uma observação em BACKUP_PATH:

No Kotlin, os únicos tipos que podem ser const são primitivos, como int, float e String. Nesse caso, como BACKUP_PATH é uma string, podemos melhorar o desempenho usando const val em vez de um val anotado com @JvmField, sem deixar de acessar o valor como um campo.

Vamos mudar isso agora em Repository.kt:

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

Se observarmos o UseCase.java, veremos que ainda resta um erro.

O erro final diz Exception: 'java.io.IOException' is never thrown in the corresponding try block.

No entanto, se observarmos o código da Repository.saveAs em Repository.kt, veremos que ele gera uma exceção. O que está acontecendo?

Java tem o conceito de uma "exceção verificada". São exceções que podem ser recuperadas, como a digitação do usuário por um nome de arquivo ou a indisponibilidade da rede. Depois que uma exceção verificada é detectada, o desenvolvedor pode enviar feedback ao usuário sobre como corrigir o problema.

Como as exceções verificadas são verificadas no momento da compilação, declare-as na assinatura do método:

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

O Kotlin, por outro lado, não tem exceções verificadas, e é isso que está causando o problema aqui.

A solução é pedir ao Kotlin para adicionar a IOException que pode ser gerada à assinatura do Repository.saveAs(), de modo que o bytecode da JVM o inclua como uma exceção verificada.

Para fazer isso, use a anotação @Throws do Kotlin, que ajuda na interoperabilidade entre Java e Kotlin. No Kotlin, as exceções se comportam de maneira semelhante ao Java, mas, ao contrário do Java, o Kotlin tem apenas exceções desmarcadas. Então, se você quiser informar ao código Java que uma função do Kotlin gera uma exceção, será necessário usar a anotação @Throws à assinatura da função Kotlin, mudar para o Repository.kt file e atualizar a saveAs() para incluir a nova anotação:

@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...
}

Com a anotação @Throws, podemos ver que todos os erros do compilador em UseCase.java foram corrigidos. Oba!

Você pode se perguntar se precisará usar blocos try e catch ao chamar saveAs() a partir do Kotlin agora.

Não. Lembre-se: o Kotlin não verifica as exceções, e a adição de @Throws a um método não muda isso:

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

Ainda é útil capturar exceções quando elas puderem ser processadas, mas o Kotlin não força você a lidar com elas.

Neste codelab, abordamos os princípios básicos de como escrever código Kotlin, que também é compatível com a programação de código Java idiomático.

Conversamos sobre como podemos usar anotações para mudar a forma como o Kotlin gera o bytecode da JVM, como:

  • @JvmStatic para gerar membros e métodos estáticos.
  • @JvmOverloads para gerar métodos sobrecarregados para funções com valores padrão.
  • @JvmName para mudar o nome de getters e setters.
  • @JvmField para expor uma propriedade diretamente como um campo, e não via getters e setters.
  • @Throws para declarar exceções verificadas.

O conteúdo final dos nossos arquivos é:

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
}