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,@JvmStatice 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:
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.javafriendlykotlinPodemos 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.javafriendlykotlinAgora, 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 groupsIsso 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 groupsO método alternativo é especialmente útil para propriedades que usam um getter padrão definido implicitamente. Exemplo:
@get:JvmName("isActive")
val active: BooleanIsso 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:
@JvmStaticpara gerar membros e métodos estáticos.@JvmOverloadspara gerar métodos sobrecarregados para funções com valores padrão.@JvmNamepara mudar o nome de getters e setters.@JvmFieldpara expor uma propriedade diretamente como um campo, e não via getters e setters.@Throwspara 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
}