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:
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
}