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

  1. Prefira tipos non-nullable sempre que possível
  2. Evite !! — quase sempre existe uma alternativa melhor
  3. Use ?.let para operações condicionais
  4. Use Elvis ?: para valores padrão
  5. Valide cedo: converta nullable para non-nullable no início da função
  6. Aproveite require e check para 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!