Como chamar o código Kotlin usando Java

Neste codelab, você vai aprender a escrever ou adaptar seu código Kotlin para facilitar as chamadas do código Java.

O que você vai aprender

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

O que você já precisa saber

Este codelab foi escrito para programadores e pressupõe um 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, vamos ter um único arquivo .java chamado UseCase.java, que vai representar a base de código atual.

Vamos imaginar que acabamos de substituir uma funcionalidade originalmente escrita em Java por uma nova versão escrita em Kotlin e precisamos terminar de integrá-la.

Importar o projeto

O código do projeto pode ser clonado do projeto do GitHub aqui: GitHub

Outra opção é baixar e extrair o projeto de um arquivo zip encontrado aqui:

Fazer o download do ZIP

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

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

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

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: "Non-static cannot be accessed from a static context".

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

Vemos que nosso repositório é um singleton declarado usando a palavra-chave "object". O problema é que o Kotlin está gerando uma instância estática dentro da nossa classe, em vez de expor essas instâncias 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áticos ao anotar 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 seu código usando o ambiente de desenvolvimento integrado.

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

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

Vamos considerar 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, transformamos os métodos em funções de extensão. O Java não tem funções de extensão, então o Kotlin compila esses métodos como funções estáticas.

Infelizmente, se analisarmos o método registerGuest() em UseCase.java, vamos notar 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" ou de pacote dentro de uma classe cujo nome é baseado no nome do arquivo. Nesse caso, como o arquivo se chama StringUtils.kt, a classe correspondente é chamada de StringUtilsKt.

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

  • Pode haver muitos lugares no nosso código que precisam ser atualizados.
  • O nome em si é estranho.

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

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

package com.google.example.javafriendlykotlin

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

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

Agora, se voltarmos para UseCase.java, vamos ver que o erro de StringUtils.nameToLogin() foi resolvido.

Infelizmente, esse erro foi substituído por um novo sobre os parâmetros transmitidos ao construtor de User. Vamos continuar para a próxima etapa e corrigir esse último erro em UseCase.registerGuest().

O Kotlin é compatível com valores padrão para parâmetros. Para saber como eles são usados, confira o bloco 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")))

Para o usuário "warlow", podemos pular 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 em Java.

UseCase.java:

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

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

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

Como a classe User tem apenas um construtor principal, e ele não inclui anotações, a palavra-chave constructor foi omitida. Agora que queremos adicionar uma anotação, 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, vamos 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 leia mais para saber 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);
}

É possível criar um User com apenas dois parâmetros, id e username:

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

Também é possível construir um User incluindo um terceiro parâmetro para displayName e usando o valor padrão para groups:

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

Mas não é possível pular displayName e fornecer um valor para groups sem escrever código adicional:

Então, vamos excluir essa linha ou precedê-la com "//" para comentar.

Em Kotlin, se quisermos combinar parâmetros padrão e não padrão, precisamos 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 gera sobrecargas para funções, incluindo construtores, mas cria apenas uma sobrecarga por parâmetro com um valor padrão.

Vamos voltar a 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 é um erro interessante! Se você usar o recurso de preenchimento automático do ambiente de desenvolvimento integrado na classe User, vai notar que hasSystemAccess() foi renomeado para getHasSystemAccess().

Para corrigir o problema, gostaríamos que o Kotlin gerasse um nome diferente para a propriedade val hasSystemAccess. Para isso, podemos usar a anotação @JvmName. Vamos voltar para User.kt e ver onde ele deve ser aplicado.

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

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

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

Como alternativa, é possível aplicar à propriedade usando um prefixo get: desta forma:

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

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

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

Isso permite que o nome do getter seja mudado sem precisar definir um getter explicitamente.

Apesar dessa distinção, use o que for mais conveniente para você. Ambos farão com que o Kotlin crie um getter com o nome hasSystemAccess().

Se voltarmos para UseCase.java, poderemos verificar que getSystemUsers() agora está sem erros.

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

Nomenclatura de getter e setter

Ao programar em Kotlin, é fácil esquecer que escrever um 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 Ferramentas > Kotlin > Mostrar bytecode do Kotlin no menu e clique no botão Descompilar:

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

Quando queremos acessar esses elementos no Java, precisamos escrever explicitamente o nome do getter.

Na maioria dos casos, o nome Java dos 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 cujos nomes começam com "is". Nesse caso, o nome Java do 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 bytecode com 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, considere a seguinte classe:

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

Imagine que queremos mudar o nome do setter de setRed() para updateRed(), sem alterar os getters. 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
}

Em Java, poderíamos escrever:

color.updateRed(0.8f);

UseCase.formatUser() usa acesso direto a campos para receber os valores das propriedades de um objeto User.

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

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

Como os objetos User são imutáveis, queremos expor cada uma das propriedades deles como campos. Por isso, vamos anotar 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 voltarmos a UseCase.formatUser(), vamos ver que os erros foram corrigidos.

@JvmField ou const

Com isso, há outro erro de aparência semelhante no arquivo UseCase.java:

Repository.saveAs(Repository.BACKUP_PATH);

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

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

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

Se olharmos para UseCase.java agora, vamos ver que o erro desapareceu, mas também há uma nota em BACKUP_PATH:

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

Vamos mudar isso agora em Repository.kt:

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

Se analisarmos UseCase.java, veremos que só resta um erro.

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

Se analisarmos o código de Repository.saveAs em Repository.kt, veremos que ele gera uma exceção. O que está acontecendo?

O Java tem o conceito de "exceção verificada". Essas são exceções que podem ser recuperadas, como o usuário digitar errado um nome de arquivo ou a rede ficar temporariamente indisponível. Depois que uma exceção verificada é detectada, o desenvolvedor pode fornecer feedback ao usuário sobre como corrigir o problema.

Como as exceções verificadas são verificadas no momento da compilação, elas são declaradas 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 de Repository.saveAs(), para que o bytecode da JVM a inclua como uma exceção verificada.

Fazemos isso com a anotação @Throws do Kotlin, que ajuda na interoperabilidade entre Java/Kotlin. Em Kotlin, as exceções se comportam de maneira semelhante ao Java, mas, ao contrário do Java, o Kotlin tem apenas exceções não verificadas. Portanto, se você quiser informar ao seu código Java que uma função Kotlin gera uma exceção, use a anotação @Throws na assinatura da função Kotlin. Mude para Repository.kt file e atualize 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!

Talvez você esteja se perguntando se terá que usar blocos try e catch ao chamar saveAs() do Kotlin agora.

Não. O Kotlin não tem exceções verificadas, e adicionar @Throws a um método não muda isso:

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

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

Neste codelab, abordamos os princípios básicos de como escrever código Kotlin que também oferece suporte à escrita de código Java idiomático.

Falamos sobre como 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
}