Trilha de Kotlin · parte 5 de 59
Null safety e tipos nuláveis
Aula da fase 1 da trilha de Kotlin: Como `String?` muda o contrato de uma função, por que `!!` esconde uma decisão de modelagem e como tratar valores vindos do Java?
iniciantePublicada em · 14 min de leitura
Null safety e tipos nuláveis
Imagine um formulário com um campo “telefone” que pode ficar em branco. Se você entrega esse formulário a outra pessoa sem dizer se o campo é obrigatório, ela vai ter de adivinhar. Algumas vezes o telefone estará lá, outras vezes não, e a única forma de descobrir é tentar usar e ver se explode. Em muitas linguagens, uma referência a objeto funciona exatamente assim: qualquer variável pode guardar “nada” sem que o tipo declare isso. O “nada” tem nome, é o null, e o erro que ele causa em Java tem nome também: NullPointerException.
Esta aula trata desse problema no Kotlin. A ideia central é simples de dizer e exige cuidado para aplicar: o tipo de uma variável precisa dizer se ela pode ser nula. Quando o tipo diz isso, o compilador faz a checagem que antes ficava por conta da memória de quem escreveu o código. Tony Hoare, que incluiu referências nulas na linguagem ALGOL W em 1965, chamou a invenção de “erro de um bilhão de dólares” numa palestra de 2009. Você vai ver que o Kotlin não elimina o null do runtime da JVM, mas muda quem precisa lembrar dele.
O mapa desta aula segue três perguntas da trilha. A primeira é como String? muda o contrato de uma função. A segunda é por que o operador !! esconde uma decisão de modelagem. A terceira é como tratar valores que chegam do Java, onde o compilador do Kotlin não consegue saber se o valor é nulo.
1. o problema que o null esconde
Em Java, qualquer referência de objeto pode ser null, e o tipo não diz nada sobre isso. Considere um método que busca o e-mail de um usuário. A assinatura String buscarEmail(int id) promete uma String, mas nada impede o método de devolver null quando o usuário não existe. Quem chama o método é obrigado a lembrar disso, e a única fonte de informação costuma ser a documentação, que ninguém lê depois da terceira vez.
O custo aparece longe da origem. O método devolve null numa madrugada, o código que recebe esse valor só tenta usar o tamanho do e-mail alguns passos depois, e a exceção aponta para uma linha que é inocente. Quem depura vê um NullPointerException sem saber de onde o nulo veio. Em serviços de backend, isso significa uma requisição que falha com erro 500 e um log que não ajuda.
Go tem um problema parecido com o nil em ponteiros, mapas e interfaces. A diferença é que o Go não tenta separar, no tipo, o que pode e o que não pode ser nulo. Por isso o tratamento de erro idiomático do Go depende de disciplina, como você viu em Tratamento de Erros Idiomático. No Kotlin, a disciplina continua importante, mas o compilador passa a cobrar a parte que pode verificar sozinho.
2. dois tipos para duas situações
A solução do Kotlin é ter dois tipos onde o Java tem um só. String é uma string que nunca é nula. String? é uma string que pode ser nula. O ? faz parte do tipo, não é uma anotação decorativa, e o compilador trata os dois como tipos diferentes.
Veja o efeito no contrato de uma função. Suponha que a busca devolva String? e que outra função exija String:
fun buscarEmail(id: Int): String? = if (id == 1) "ana@exemplo.com" else null
fun tamanhoDoEmail(email: String): Int = email.length
fun main() {
println(tamanhoDoEmail(buscarEmail(1)))
}
Esse código não compila. O compilador responde com a mensagem argument type mismatch: actual type is 'String?', but 'String' was expected. Nenhum teste precisa rodar para descobrir o problema. A assinatura de tamanhoDoEmail virou uma garantia: quem a chama precisa entregar uma string de verdade, e a função não precisa se proteger de null por dentro. O contrato deixou de ser um comentário e passou a ser código que o compilador verifica.
Esse é o primeiro ganho da aula. Quando você escreve String? numa assinatura, está dizendo a quem chama que ele precisa tratar a ausência. Quando escreve String, está prometendo que o valor existe. Um bom projeto Kotlin tem poucos tipos nuláveis em seu núcleo e os concentra nas bordas, onde o dado realmente pode faltar, como um campo opcional de formulário ou o resultado de uma busca que pode não achar nada.
3. como tratar um valor que pode ser nulo
Com um String? nas mãos, o Kotlin oferece algumas ferramentas. Cada uma responde a uma pergunta diferente, e a escolha certa depende do que o seu código deve fazer quando o valor está ausente.
O operador ?. é o acesso seguro. email?.length devolve null se email for null, e o tamanho se houver valor. O tipo do resultado é Int?, porque o acesso pode não acontecer. Ele é útil quando a ausência é um resultado aceitável e o próximo passo também pode lidar com null.
O operador ?:, chamado de elvis por causa do rosto que o forma de lado, define um valor padrão para o caso de ausência: val tamanho = email?.length ?: 0. Lê-se como “o tamanho, ou zero se não houver e-mail”. O lado direito também aceita throw e return, o que permite transformar a ausência em erro ou em saída antecipada do método, sem criar uma variável temporária.
Quando uma checagem explícita deixa o código claro, o compilador entende a checagem e aplica o que é chamado de smart cast. Depois de if (outro != null), dentro do bloco a variável tem o tipo String, não String?, e você pode passá-la para tamanhoDoEmail sem nenhum operador extra. O compilador só faz isso quando consegue garantir que a variável não mudou entre a checagem e o uso. Variáveis var que podem ser alteradas por outro código deixam essa garantia mais difícil, e é por isso que o smart cast funciona melhor sobre val.
Para conversões de tipo com possibilidade de falha existe as?, que devolve null em vez de lançar exceção. "abc" as? Int vira null, enquanto "abc" as Int lança ClassCastException. E para validar pré-condições existe requireNotNull(valor) { "mensagem" }, que lança IllegalArgumentException com a mensagem que você escreveu. Um exemplo real: ao processar o pedido 2, o e-mail do cliente é obrigatório, então a função deve falhar com uma mensagem que aponte para o pedido, e não com um NullPointerException vindo de dentro de uma biblioteca.
Rodei esses exemplos com o kotlinc 2.1.21 e a saída foi esta:
tamanho com elvis: 0
smart cast: 15
NPE: NullPointerException
as?: null
requireNotNull: email é obrigatório para o pedido 2
Repare que nenhum desses operadores escondeu o problema. O ?: declarou um padrão, o requireNotNull declarou uma falha com contexto, e o as? declarou que a conversão pode não dar certo. Cada um deixa uma intenção escrita no código, e isso é o que a trilha chama de tratar ausência como decisão de modelagem.
O diagrama resume esses caminhos. A busca devolve String?, e o losango pergunta se o valor é nulo. Os ramos de tratamento seguro são o smart cast, quando o valor existe, o valor padrão e a falha com mensagem, quando ele não existe. A seta tracejada rosa é o !!, que pula a pergunta e aposta que o valor está lá. Quando a aposta perde, o caminho termina no NullPointerException, o único ramo do desenho sem tratamento.
4. o operador !! e a decisão que ele esconde
O !! diz ao compilador: “trate este valor como não nulo, eu sei o que estou fazendo”. Se o valor for null naquele momento, o programa lança NullPointerException. O operador existe porque há situações em que o programador conhece algo que o compilador não consegue provar, mas usá-lo como atalho para silenciar um erro é o que a trilha chama de sinal de modelagem fraca.
O problema é que !! não resolve a dúvida. Ele a empurra para o tempo de execução. Imagine uma cadeia como pedido!!.cliente!!.endereco!!.cidade. Cada !! é uma pequena aposta de que o dado está lá. Se o cliente não tem endereço cadastrado, a aposta perde no meio da madrugada, e o log mostra só a linha da cadeia. A pergunta certa era outra: o pedido pode existir sem cliente? O cliente pode existir sem endereço? Se a resposta for sim, o tipo deveria dizer Endereco?, e o código deveria tratar essa ausência.
Há um caso em que !! faz sentido: quando você tem uma verificação que o compilador não enxerga, e a própria lógica do programa garante que o valor existe. Mesmo assim, costuma ser melhor trocar o !! por checkNotNull ou requireNotNull com mensagem, porque a falha ganha um texto que aponta para a causa. Se você se pegar escrevendo !! em todo lugar, o problema não está na sintaxe. Ele está no modelo, que deixou de dizer quais dados podem faltar.
5. valores que vêm do Java
Até aqui, o compilador sabia de cada tipo porque o código era Kotlin. O problema real surge na fronteira com o Java, porque o compilador Kotlin lê a assinatura de um método Java e não encontra nada que diga se o retorno pode ser nulo. Para esses casos, a documentação oficial chama o tipo de platform type, escrito T!. Ele significa “T ou T?”, e o Kotlin relaxa as checagens sobre ele. Você pode atribuí-lo a um String ou a um String?, e a falha só aparece quando o valor é usado.
Veja um método Java sem anotação e como o Kotlin o trata:
public class Banco {
public static String nomeDoBanco(boolean vazio) { return vazio ? null : "pedidos"; }
}
fun main() {
val tamanho = Banco.nomeDoBanco(true)?.length
println("safe call em platform type: $tamanho")
val nome: String = Banco.nomeDoBanco(true)
println("atribuição: $nome")
}
O primeiro println imprime null, porque o ?. funciona com qualquer valor. O segundo não chega a imprimir nada. A atribuição a um tipo não nulo dispara uma exceção, e a saída real foi esta:
safe call em platform type: null
Exception in thread "main" java.lang.NullPointerException: nomeDoBanco(...) must not be null
A mensagem informa o método de origem, o que ajuda, mas a exceção aparece no ponto em que o Kotlin tentou tratar o valor como não nulo. Se o método Java tivesse usado o null numa parte distante do código, a falha apareceria lá.
A saída prática é declarar a intenção na fronteira. Se o método Java tem uma anotação de nulidade, como @Nullable das ferramentas da JetBrains ou as do JSpecify e do JSR-305, o Kotlin passa a enxergar o retorno como String? de verdade, e o compilador obriga o tratamento. O caminho inverso também vale: quando o Kotlin expõe uma função que devolve String?, o bytecode gerado carrega a anotação org/jetbrains/annotations/Nullable, e o código Java que consome essa função recebe a mesma informação. Verifiquei isso com javap -v, lendo a descrição da função gerada.
O diagrama mostra os três caminhos de um valor vindo do Java. O retângulo azul é o método Java sem anotação, o hexágono é o tipo que o Kotlin vê, e a seta vermelha é a única que termina em exceção. O caminho verde, com ?., é o que a aula recomenda para qualquer dado que possa faltar.
Pergunta de entrevistaPor que o operador !! é considerado um cheiro de código?
O !! diz ao compilador que o valor não é nulo e lança NullPointerException se ele for. Ele esconde a decisão: o código afirma que tem certeza, mas não diz o que fazer quando estiver errado. Prefiro o operador ?. com um valor padrão, ou um erro com mensagem clara, como requireNotNull. Quando um valor pode ser nulo pela regra de negócio, o tipo deve refletir isso, e o !! aparece muito pouco.
6. erros comuns e boas práticas
O primeiro erro é usar !! como resposta padrão ao compilador. Ele faz o código compilar sem fazer o programa funcionar melhor, e transfere o diagnóstico para produção. Se o compilador reclama de um String?, a pergunta é se o valor pode mesmo faltar naquele ponto. Quase sempre a resposta está no modelo, e não na chamada.
O segundo erro é combinar ?. com !! na mesma expressão, como em usuario?.endereco!!. O ?. afirma que a ausência é possível, e o !! afirma que não é. As duas afirmações se contradizem e o resultado é uma exceção no caso em que o ?. estava tentando ser seguro.
O terceiro erro é usar lateinit como se fosse um tipo nulável sem ?. lateinit var promete que a propriedade será inicializada antes do primeiro uso, e a violação dessa promessa aparece como UninitializedPropertyAccessException. É uma ferramenta útil para injeção feita por framework, em que a inicialização acontece depois do construtor, mas não serve para esconder ausência de dados.
O quarto erro é o as sem ? sobre um valor nulável. Ele lança NullPointerException ou ClassCastException conforme o caso, e o código parece seguro porque tem um cast. Quando a ausência é esperada, as? ou um ?: com exceção de domínio expressam melhor a intenção.
As boas práticas que decorrem disso são duas. A primeira é concentrar os tipos nuláveis nas bordas: entrada de API, respostas de banco, campos opcionais, e converter cedo para tipos não nulos com ?: ou com uma validação que lança uma exceção de domínio. A segunda é declarar a falha com mensagem: requireNotNull e checkNotNull deixam a exceção com um texto que aponta para o problema, e checkNotNull lança IllegalStateException no lugar de IllegalArgumentException, que é a escolha certa quando o erro está no estado do objeto e não no argumento recebido.
7. exercícios
Pegue um método Java de uma biblioteca que você usa, como um cliente HTTP ou um leitor de configuração, e escreva a chamada em Kotlin sem !!. Para cada retorno nulável que você encontrar, decida se a ausência é erro, valor padrão ou resultado normal, e escreva o tratamento correspondente.
Reescreva uma cadeia de !! como pedido!!.cliente!!.endereco!!.cidade modelando quais campos podem ser nulos. Depois escreva as classes com tipos nuláveis só onde a regra de negócio permite ausência, e confirme que o compilador força o tratamento em cada uso.
Escreva uma função fun ler(texto: String?): Int que devolve um número ou lança uma exceção com mensagem clara quando o texto é nulo ou inválido. Compare a versão com !! e a versão com requireNotNull, e explique por que a segunda aparece melhor no log.
8. glossário
Tipo nulável: tipo com ? no final, como String?, que aceita o valor null além dos valores do tipo base.
Safe call (?.): acesso a membro que devolve null quando o receptor é null, em vez de lançar exceção.
Elvis (?:): operador que devolve o valor da esquerda quando ele não é null e a expressão da direita caso contrário.
Smart cast: conversão automática de tipo feita pelo compilador depois de uma checagem, por exemplo de String? para String dentro de um if (x != null).
Platform type (T!): tipo que o Kotlin atribui a valores vindos de Java quando a nulidade não é declarada, e que pode ser nulo em tempo de execução.
!!: operador que força um tipo nulável a ser tratado como não nulo, lançando NullPointerException se o valor for null.
Fontes
Consultadas em 20/10/2026.
- Documentação oficial de null safety: https://kotlinlang.org/docs/null-safety.html
- Documentação oficial de interop com Java, com platform types e anotações de nulidade: https://kotlinlang.org/docs/java-interop.html
- Versão estável listada na documentação oficial de releases: 2.4.21, de 8 de outubro de 2026: https://kotlinlang.org/docs/releases.html
Os exemplos foram compilados e executados com o kotlinc 2.1.21, que vem empacotado no plugin Kotlin do IntelliJ IDEA instalado na máquina. Não testei com a 2.4.21, mas os recursos usados aqui existem desde o Kotlin 1.x e não dependem das mudanças da série 2.x.
Relacionados
- Trilha de Kotlin
- Curriculo de um Desenvolvedor Kotlin
- data class, enum e value class
- Tratamento de Erros Idiomático