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