Pular para o conteúdo

Trilha de Kotlin · parte 21 de 59

Expressões e interop com Java

Aula da fase 3 da trilha de Kotlin: Como `if`, `when` e `try` produzem valores, e como expor uma API Kotlin de forma amigável para quem ainda chama pelo Java?

intermediárioPublicada em · 12 min de leitura

Tamanho da fonte:
Largura da coluna:
Tom:
Espaço entre linhas:
Fonte:

Expressões e interop com Java

Em Java, if é um comando. Ele decide qual instrução executar, mas não produz valor. Para guardar um resultado condicional, você declara a variável antes, deixa ela sem valor ou com um padrão, e preenche dentro de cada ramo. Em Kotlin, o mesmo if pode aparecer do lado direito de um =, como se fosse uma palavra numa frase: a expressão vira valor, e a variável nasce pronta. Essa diferença parece cosmética, mas ela muda como você escreve quase todo código, porque reduz a necessidade de variáveis que existem só para carregar um valor condicional.

A segunda metade da aula é sobre a fronteira. Kotlin roda na JVM, e a maioria das equipes de backend ainda tem código Java que chama o seu. Uma API Kotlin precisa funcionar para quem a chama de Kotlin e para quem a chama de Java, e algumas decisões de design tornam a vida do segundo grupo bem mais simples. A trilha pede duas respostas: como if, when e try produzem valores, e como expor uma API Kotlin de forma amigável para quem ainda chama pelo Java. Os exemplos foram compilados com o kotlinc 2.1.21, e a parte de interop foi testada de verdade com um arquivo Java chamando o código Kotlin.

1. o problema que as expressões resolvem

Considere um código Java que escolhe a porta de um serviço a partir de uma string de configuração, usando um valor padrão se a conversão falhar. Uma forma comum de escrever é declarar a variável antes, com um valor provisório, e preenchê-la dentro de um try com um catch que sobrescreve o valor. A variável precisa ser mutável, e o leitor precisa acompanhar em qual ponto ela recebe o valor final.

Com expressões, o mesmo problema vira uma definição única. A variável é val, nasce com o valor certo e nunca muda. Isso tem um efeito prático grande: o compilador garante que a variável sempre tem um valor, e o leitor não precisa rastrear reatribuições. Em serviços de backend, esse tipo de clareza aparece em toda leitura de configuração, em todo mapeamento de status HTTP e em toda conversão de entrada.

Há outro efeito, menos óbvio. Como o if e o when devolvem valor, funções que antes exigiam um corpo com return em cada ramo passam a ser escritas como uma única expressão. A função fica curta, e a intenção aparece no formato da expressão, não numa sequência de instruções.

2. if, when e try como expressões

Um bloco em Kotlin produz como valor a última expressão que ele contém. Isso vale para funções, para lambdas e para os ramos de if, when e try. O que decide se um if é expressão é o contexto onde ele aparece: se o resultado é usado, como em uma atribuição ou em um return, o compilador exige que ele tenha else. Sem o ramo, o código não compila:

fun main() {
    val x = 3
    val rotulo = if (x > 2) "grande"
    println(rotulo)
}

O compilador responde com 'if' must have both main and 'else' branches when used as an expression. A regra existe por uma razão simples: se o if for usado como valor e a condição for falsa, a variável não teria o que receber. A exigência de else torna isso impossível em tempo de compilação.

O when segue a mesma lógica. Quando ele é expressão, precisa cobrir todos os casos, e o else aparece como o ramo que trata o restante. Aqui vale a regra da aula anterior: um when sobre um tipo que você não controla precisa de else, e um when sobre uma sealed pode dispensá-lo. Veja a classificação de códigos HTTP, que é um caso típico de expressão com intervalos:

fun classificar(status: Int): String = when (status) {
    200, 201 -> "ok"
    in 400..499 -> "erro do cliente"
    in 500..599 -> "erro do servidor"
    else -> "outro"
}

Com classificar(404), o código Java que consome essa função recebe erro do cliente. A expressão substitui uma cadeia de if com return em cada linha, e a função inteira cabe no formato de uma definição.

O try também é expressão, e é aí que o conceito fica mais interessante. O valor do try é a última expressão do bloco, e o valor do catch é a última expressão dele. O resultado deve ter um tipo compatível com os dois:

fun lerPorta(texto: String): Int = try {
    texto.toInt()
} catch (e: NumberFormatException) {
    8080
}

Com lerPorta("abc"), o toInt lança a exceção e o catch devolve 8080. Esse código não precisa de variável temporária, e a função deixa explícito que o único resultado possível é um Int.

Falta falar de uma peça que aparece em todos esses exemplos sem ter sido nomeada: o tipo Nothing. Uma expressão que nunca termina normalmente, como throw, return, break ou continue, tem tipo Nothing. Esse tipo é subtipo de qualquer outro, por isso ele cabe em qualquer ramo. Veja:

fun validar(x: Int): Int = if (x >= 0) x else throw IllegalArgumentException("negativo")

O ramo do throw não produz valor, então o tipo do if continua sendo Int, e o compilador aceita. Na prática, Nothing é o que permite usar throw como parte de uma expressão, como no operador ?: que você viu na aula sobre null safety.

try e if produzem valor. texto → toInt() lança?; toInt() lança? → Int do try (não lança); toInt() lança? → catch (lança); Int do try → val porta: Int…
figura 1 · try e if produzem valor

O diagrama mostra dois exemplos lado a lado. Em lerPorta, o losango é a pergunta do try, e os dois caminhos convergem numa mesma variável porta, que nunca é reatribuída. Em validar, o throw aparece em rosa, porque o caminho termina sem produzir valor. A seta tracejada até valor mostra que ela nunca chega, e é justamente isso que faz o tipo Nothing caber no if.

3. o que a API Kotlin vira no bytecode

Até aqui, tudo foi código Kotlin. Agora a segunda parte da trilha, que é como o Java enxerga o que você escreveu. O compilador transforma cada construção Kotlin em bytecode, e o código Java só vê esse bytecode. Entender essa tradução é o que evita surpresas como um método que o Java não consegue chamar.

Para verificar, escrevi uma pequena API em Kotlin e um arquivo Java que a usa:

class Usuario(val nome: String, val ativo: Boolean) {
    @JvmField val codigo: Int = 1

    companion object {
        @JvmStatic
        fun criar(nome: String): Usuario = Usuario(nome, true)
    }
}

class Relatorio {
    @Throws(IOException::class)
    fun gerar() {
        println("relatorio gerado")
    }
}

object Saudacao {
    @JvmOverloads
    @JvmStatic
    fun saudar(nome: String, saudacao: String = "Olá"): String = "$saudacao, $nome"
}

Os resultados revelaram quatro regras que valem a pena memorizar. A primeira é sobre o nome dos getters. A propriedade val ativo: Boolean vira getAtivo(), como se espera de qualquer propriedade. Mas a propriedade val isAtivo: Boolean, cujo nome já começa com is, vira isAtivo(), sem o prefixo get. Num primeiro teste, eu supus que ativo viraria isAtivo(), e o javac recusou o código com cannot find symbol. Por isso o nome da propriedade precisa ser pensado já de olho em quem vai chamar pelo Java.

A segunda regra é sobre o companion object. Um método marcado com @JvmStatic dentro do companion vira um método estático da própria classe. O javap mostrou public static final Usuario criar(java.lang.String) dentro de Usuario, e o Java chama Usuario.criar("Ana") sem nenhuma referência ao Companion. Sem a anotação, o Java precisaria escrever Usuario.Companion.criar(...), o que não é o que você quer expor.

A terceira regra é sobre parâmetros com valor padrão. O Kotlin aceita saudar(nome) e saudar(nome, saudacao) com um só método, mas a JVM não tem parâmetros opcionais. Sem anotação, o Java só vê a assinatura completa. Com @JvmOverloads, o compilador gera uma sobrecarga para cada parâmetro com default, e o javap mostrou saudar(String, String) e saudar(String). O Java chamou as duas, e a saída foi Olá, Bia e Oi, Bia.

A quarta regra é sobre o tipo de retorno Unit. Um método com fun marcar(): Unit vira void no bytecode, e o Java não consegue atribuir o retorno a nada. Escrever Object r = new Estado().marcar(); gerou o erro incompatible types: void cannot be converted to Object. O tipo Unit existe no Kotlin para que funções sem valor de retorno também sejam expressões, mas na fronteira ele desaparece e vira void.

Kotlin visto pelo Java. val ativo: Boolean → getAtivo(); val isAtivo: Boolean → isAtivo(); companion @JvmStatic criar() → Usuario.criar(String)…
figura 2 · Kotlin visto pelo Java

O diagrama põe as regras lado a lado. À esquerda, o código Kotlin que você escreve; à direita, a forma que o Java enxerga. As setas são iguais de propósito, porque cada linha da esquerda tem um correspondente direto na direita. A diferença está nos nomes e nas assinaturas, e é nessa diferença que a interop costuma falhar quando ninguém pensa no chamador Java.

4. exceções e nulabilidade na fronteira

Duas regras da fronteira merecem atenção separada, porque afetam a segurança do código. A primeira é sobre exceções. Kotlin não tem exceções verificadas, então um método Kotlin que lança IOException não declara isso na assinatura. O Java, por sua vez, obriga o chamador a tratar exceções verificadas. Sem @Throws, o Java acredita que o método não lança nada, e o erro só aparece em produção. Com @Throws(IOException::class), a assinatura gerada declara throws IOException, e o Java passa a exigir tratamento. O teste mostrou o resultado: o método gerar() aparece no Java como gerar() throws IOException.

A segunda regra é sobre nulabilidade. O compilador escreve anotações de nulidade no bytecode. Um fun achar(id: Int): String? gera um método cujo descritor carrega org/jetbrains/annotations/Nullable. Ferramentas e IDEs Java que leem essa anotação avisam o desenvolvedor de que o retorno pode ser nulo, e um tipo não nulo é marcado de forma equivalente. Essa é a razão pela qual a aula Null safety e tipos nuláveis insiste em String? nas assinaturas: o tipo do Kotlin vira informação que o Java consegue ler.

Há um cuidado com Optional. O Kotlin não converte String? em java.util.Optional automaticamente. Se a equipe Java espera Optional, a conversão precisa ser escrita, e ela é uma escolha de API, não um detalhe de tradução. Quando o consumidor é Java, faça essa escolha de forma explícita e documente, porque um tipo nulável em Kotlin e um Optional em Java expressam a mesma ideia com tipos diferentes.

5. erros comuns e boas práticas

O primeiro erro é usar if como expressão sem else, por hábito de Java. O compilador recusa o código, mas a mensagem pode parecer estranha para quem vem de uma linguagem em que o if nunca produz valor. A correção é dar um valor para o caso falso, ou separar a lógica em uma função com retorno claro.

O segundo erro é escrever try como expressão e engolir a exceção com um valor sem sentido, como um catch que devolve 0 para qualquer falha. O valor padrão esconde o problema e produz dados errados silenciosamente. Use o valor padrão apenas quando a ausência for um resultado normal, e registre ou relance a exceção nos demais casos.

O terceiro erro é esquecer @JvmOverloads em uma função com parâmetros padrão exposta ao Java. O Kotlin continua funcionando, e o Java só descobre a falta quando tenta chamar a versão reduzida e encontra cannot find symbol. Se a classe é consumida por Java, a anotação deve ser parte da revisão.

O quarto erro é nomear propriedades booleanas sem pensar no getter. Uma propriedade ativo e uma isAtivo geram nomes diferentes no Java, e uma delas pode quebrar código que já existia. Escolha o nome com a convenção que o Java espera e confirme com javap.

O quinto erro é esquecer @Throws em métodos que lançam exceções verificadas do Java. O Kotlin não avisa, e o chamador Java só descobre o problema quando recebe a exceção sem ter sido avisado.

A boa prática que resume a aula é desenhar a API como uma expressão sempre que o resultado for um valor, e pensar no bytecode quando ela sair do Kotlin. O código fica mais curto e mais verificável, e quem chama pelo Java recebe nomes, nulabilidade e exceções que fazem sentido no outro lado da fronteira.

6. exercícios

Escreva uma função fun faixa(nota: Int): String que devolve "A", "B" ou "C" usando when como expressão e sem variável temporária. Depois chame-a a partir de um arquivo Java e confirme que a saída é a mesma.

Escreva uma função fun lerInt(texto: String?): Int que usa try como expressão e devolve 0 quando a conversão falha. Em seguida reescreva a função para lançar IllegalArgumentException no caso de falha, e compare qual versão é mais honesta sobre o que aconteceu.

Crie uma classe Kotlin com uma propriedade booleana chamada ativo, outra chamada isAtivo, e um método companion com @JvmStatic. Use javap -p para listar os nomes gerados e escreva, num arquivo Java, as chamadas que funcionam.

7. glossário

Expressão: construção que produz um valor, como if, when e try quando seu resultado é usado.

Nothing: tipo de expressões que nunca terminam normalmente, como throw, e que por isso cabe em qualquer ramo de tipo.

Interop: conjunto de regras que definem como código Kotlin e código Java se chamam mutuamente na mesma JVM.

@JvmStatic: anotação que gera um método estático na classe a partir de uma função de companion object ou de object.

@JvmOverloads: anotação que gera sobrecargas para cada parâmetro com valor padrão, permitindo que o Java chame a função com menos argumentos.

@JvmField: anotação que expõe uma propriedade como campo público no bytecode, sem getter.

@Throws: anotação que declara no bytecode as exceções verificadas que um método pode lançar, para que o Java as trate.

Unit: tipo de retorno de funções sem valor útil, que aparece como void na JVM.

Fontes

Consultadas em 20/10/2026.

A documentação de if, when e try como expressões não foi consultada nesta revisão. As regras de Nothing e de exaustividade do when foram verificadas pela execução dos exemplos no kotlinc 2.1.21. A ausência de conversão automática para Optional é descrita aqui como comportamento do compilador, e não foi conferida numa página oficial hoje.

Relacionados

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?