Se você já programou em Java, com certeza já tomou aquele susto com NullPointerException em produção. Tony Hoare, o criador do conceito de null, chamou isso de “o erro de um bilhão de dólares”. Kotlin resolveu esse problema de um jeito elegante: o sistema de tipos diferencia referências que podem ser nulas das que não podem. Vamos entender como.
O problema com null
Em linguagens como Java, qualquer referência de objeto pode ser null. Isso significa que toda vez que você acessa um método ou propriedade, existe o risco de um NPE:
// Em Java, isso compila sem problema, mas estoura em runtime:
// String nome = null;
// int tamanho = nome.length(); // NullPointerException!
Kotlin resolve isso em tempo de compilação. Se o código pode causar NPE, ele simplesmente não compila.
Tipos nullable e non-nullable
Em Kotlin, por padrão, variáveis não podem ser nulas:
var nome: String = "Karina"
// nome = null // ERRO DE COMPILAÇÃO: Null can not be a value of a non-null type String
// Para permitir null, adicione ? ao tipo
var nomeOpcional: String? = "Karina"
nomeOpcional = null // OK, o tipo permite
Essa distinção é feita no sistema de tipos: String e String? são tipos diferentes. O compilador sabe exatamente quais variáveis podem ser nulas e te obriga a lidar com isso.
Operador de chamada segura (?.)
O safe call operator é seu melhor amigo quando trabalha com tipos nullable:
val nome: String? = obterNomeDoUsuario()
// Em vez de checar manualmente:
// if (nome != null) { println(nome.length) }
// Use o operador ?.
val tamanho: Int? = nome?.length
println(tamanho) // imprime o tamanho ou null
// Encadeamento seguro
val cidade: String? = usuario?.endereco?.cidade?.uppercase()
Se qualquer valor na cadeia for null, a expressão inteira retorna null sem lançar exceção. Lindo, né?
Operador Elvis (?:)
Quando você quer um valor padrão caso algo seja null, use o operador Elvis:
val nome: String? = null
// Valor padrão simples
val nomeExibido = nome ?: "Usuário Anônimo"
println(nomeExibido) // "Usuário Anônimo"
// Combinando com safe call
val tamanho = nome?.length ?: 0
println(tamanho) // 0
// Elvis com throw ou return
fun buscarUsuario(id: Int): Usuario {
val usuario = repositorio.buscar(id) ?: throw UsuarioNaoEncontradoException(id)
return usuario
}
fun processarNome(nome: String?) {
val nomeValido = nome ?: return // sai da função se for null
println("Processando: $nomeValido")
}
O nome “Elvis” vem do formato ?: que, se você virar de lado, parece o topete do Elvis Presley. Sério!
Operador de asserção não-nula (!!)
O operador !! converte um tipo nullable para non-nullable, lançando NPE se o valor for null:
val nome: String? = "Karina"
val tamanho: Int = nome!!.length // OK, porque nome não é null
val nomeNulo: String? = null
// val boom = nomeNulo!!.length // NullPointerException!
Atenção: use !! com muita moderação! Cada !! no seu código é um NPE em potencial. Se você está usando !! com frequência, provavelmente tem algo errado no design do código.
Smart Casts
Kotlin é esperto: se você já verificou que algo não é null, ele faz smart cast automaticamente:
fun imprimir(texto: String?) {
if (texto != null) {
// Aqui o Kotlin já sabe que texto é String (não-nullable)
println(texto.length) // sem necessidade de ?. ou !!
println(texto.uppercase())
}
}
// Funciona com when também
fun descrever(valor: Any?) = when (valor) {
null -> "Nulo"
is String -> "String com ${valor.length} caracteres" // smart cast para String
is Int -> "Inteiro: ${valor * 2}" // smart cast para Int
else -> "Outro tipo"
}
let, also e outros scope functions com null
A função let é muito usada para executar código somente quando o valor não é null:
val email: String? = obterEmailDoUsuario()
// Executa o bloco apenas se email não for null
email?.let { emailValido ->
enviarNotificacao(emailValido)
registrarLog("Email enviado para $emailValido")
}
// Versão mais concisa usando it
email?.let {
enviarNotificacao(it)
}
Coleções e null safety
Kotlin diferencia coleções que contêm elementos nullable daquelas que não contêm:
val nomes: List<String> = listOf("Ana", "Bruno", "Carla")
val nomesOuNulos: List<String?> = listOf("Ana", null, "Carla")
// filterNotNull remove os nulos e muda o tipo
val apenasNomes: List<String> = nomesOuNulos.filterNotNull()
println(apenasNomes) // [Ana, Carla]
// listOfNotNull já filtra na criação
val lista = listOfNotNull("Ana", null, "Carla", null)
println(lista) // [Ana, Carla]
Interoperabilidade com Java
Quando você chama código Java a partir de Kotlin, os tipos vêm como platform types (indicados por !), pois o compilador não sabe se podem ser nulos:
// Retorno de método Java: String! (platform type)
val resultado = javaObject.getValor()
// Boas práticas: declare o tipo explicitamente
val resultadoSeguro: String? = javaObject.getValor() // trata como nullable
val resultadoCerto: String = javaObject.getValor() // assume não-null (pode dar NPE)
A recomendação é: quando chamar código Java, trate o retorno como nullable até ter certeza de que não será null.
Boas práticas
- Prefira tipos non-nullable sempre que possível
- Evite
!!— quase sempre existe uma alternativa melhor - Use
?.letpara operações condicionais - Use Elvis
?:para valores padrão - Valide cedo: converta nullable para non-nullable no início da função
- Aproveite
requireecheckpara validações:
fun processarPedido(pedidoId: String?, valor: Double) {
requireNotNull(pedidoId) { "ID do pedido não pode ser nulo" }
require(valor > 0) { "Valor deve ser positivo" }
// Aqui pedidoId já é String (non-nullable)
println("Processando pedido $pedidoId no valor de R$ $valor")
}
Conclusão
Null Safety é provavelmente o recurso que mais vai poupar suas noites de sono como desenvolvedor. O sistema de tipos de Kotlin te força a pensar em cenários nulos durante a codificação, não em produção. É uma mudança de mentalidade que, depois que você absorve, faz todo o sentido do mundo.
Bora codar sem medo de NPE!