Neste tutorial, vamos explorar em profundidade o sistema de null safety do Kotlin, uma das funcionalidades mais importantes e inovadoras da linguagem. NullPointerException (NPE) é historicamente um dos erros mais comuns em Java e outras linguagens. O Kotlin resolve esse problema no nível do sistema de tipos, distinguindo entre referências que podem ser nulas e referências que nunca são nulas. Você vai aprender sobre tipos nullable, safe calls, Elvis operator, non-null assertion, funções como let e also, smart casts e platform types.

Tipos Nullable e Non-Nullable

Em Kotlin, o sistema de tipos diferencia entre referências que podem conter null e referências que não podem. Por padrão, todos os tipos são non-nullable. Para permitir que uma variável contenha null, você adiciona ? ao tipo.

fun main() {
    // Tipos non-nullable: NUNCA podem ser null
    var nome: String = "Kotlin Brasil"
    // nome = null // ERRO DE COMPILAÇÃO: Null can not be a value of a non-null type

    // Tipos nullable: podem ser null
    var sobrenome: String? = "Silva"
    sobrenome = null // OK — tipo permite null

    // Non-nullable é garantido pelo compilador
    println(nome.length) // Seguro: 'nome' nunca é null

    // Nullable precisa de tratamento especial
    // println(sobrenome.length) // ERRO: Only safe (?.) or non-null asserted (!!) calls are allowed
    println(sobrenome?.length) // Seguro: retorna null se sobrenome for null
}

Essa distinção no sistema de tipos é verificada em tempo de compilação. Isso significa que a maioria dos NPEs é detectada antes mesmo do programa ser executado, o que representa uma enorme vantagem sobre linguagens como Java.

Safe Calls (?.)

O operador de safe call ?. permite acessar propriedades e chamar métodos em referências nullable de forma segura. Se a referência for null, a expressão inteira retorna null em vez de lançar uma exceção.

fun obterComprimento(texto: String?): Int? {
    return texto?.length
}

fun main() {
    val nome: String? = "Karina Melo"
    val vazio: String? = null

    println(nome?.length)      // 11
    println(vazio?.length)     // null (sem exceção)

    // Encadeamento de safe calls
    val endereco: String? = "Rua das Flores, 123"
    println(endereco?.uppercase()?.take(10)) // RUA DAS FL
    println(null as String?)                  // null (nenhum método é chamado)

    // Safe call com atribuição
    val lista: MutableList<String>? = mutableListOf("a", "b")
    lista?.add("c")    // Adiciona normalmente
    println(lista)     // [a, b, c]

    val listaNula: MutableList<String>? = null
    listaNula?.add("x") // Não faz nada, não lança exceção
    println(listaNula)   // null
}

Safe calls são encadeáveis: a?.b?.c?.d retorna null se qualquer parte da cadeia for null. Isso é especialmente útil ao navegar objetos aninhados vindos de APIs ou bancos de dados, onde qualquer nível pode ser ausente.

Elvis Operator (?:)

O Elvis operator ?: fornece um valor padrão quando a expressão à esquerda é null. O nome vem do fato de que o símbolo ?: de lado lembra o topete do cantor Elvis Presley.

fun main() {
    val nome: String? = null
    val nomeExibicao = nome ?: "Usuário Anônimo"
    println(nomeExibicao) // Usuário Anônimo

    val email: String? = "karina@kotlin.com"
    val emailExibicao = email ?: "Não informado"
    println(emailExibicao) // karina@kotlin.com (email não é null)

    // Combinando safe call com Elvis
    val texto: String? = null
    val comprimento = texto?.length ?: 0
    println("Comprimento: $comprimento") // Comprimento: 0

    // Elvis com throw para falhar rápido
    fun buscarUsuario(id: String): String? {
        return if (id == "1") "Karina" else null
    }

    val usuario = buscarUsuario("1") ?: throw IllegalArgumentException("Usuário não encontrado")
    println(usuario) // Karina

    // Elvis com return para retorno antecipado
    fun processarNome(nome: String?): String {
        val nomeValido = nome ?: return "Nome não fornecido"
        return "Processando: ${nomeValido.uppercase()}"
    }

    println(processarNome("Maria"))  // Processando: MARIA
    println(processarNome(null))     // Nome não fornecido
}

O padrão valor?.operacao ?: valorPadrao é extremamente idiomático em Kotlin e substitui boa parte das verificações if (x != null) que seriam necessárias em Java. O Elvis operator também pode ser usado com throw e return, o que permite padrões de falha rápida muito elegantes.

Non-Null Assertion (!!)

O operador !! converte um tipo nullable em non-nullable, lançando KotlinNullPointerException se o valor for null. Use com extrema cautela — ele é o único mecanismo em Kotlin que pode causar NPE intencionalmente.

fun main() {
    val nome: String? = "Kotlin"
    val nomeDefinitivo: String = nome!! // OK: nome não é null
    println(nomeDefinitivo.length)      // 6

    // PERIGO: isso lança NullPointerException
    // val nulo: String? = null
    // val boom: String = nulo!! // KotlinNullPointerException!

    // Caso de uso legítimo: quando você tem certeza da lógica
    val numeros = listOf(1, 2, 3, 4, 5)
    val encontrado: Int? = numeros.find { it > 3 }
    // Sabemos que existe pelo menos um número > 3
    println(encontrado!! * 10) // 40
}

A recomendação é evitar !! sempre que possível. Na maioria dos casos, safe calls com Elvis operator ou verificações if são alternativas melhores. O !! deve ser reservado para situações onde você tem absoluta certeza de que o valor não é null e quer que uma falha nessa garantia seja um erro explícito.

let e also para Verificações de Null

A função de escopo let é frequentemente combinada com safe call para executar um bloco de código apenas quando o valor não é null. Dentro do bloco, o valor é acessado como it e já é non-nullable.

fun enviarEmail(destinatario: String) {
    println("Email enviado para: $destinatario")
}

fun main() {
    val email: String? = "usuario@email.com"
    val emailNulo: String? = null

    // let com safe call: executa somente se não for null
    email?.let {
        println("Email tem ${it.length} caracteres")
        enviarEmail(it)
    }
    // Email tem 19 caracteres
    // Email enviado para: usuario@email.com

    emailNulo?.let {
        println("Isso nunca será impresso")
    }

    // also para efeitos colaterais mantendo a referência original
    val nomeProcessado = email?.also {
        println("Validando email: $it")
    }?.uppercase()
    println(nomeProcessado) // USUARIO@EMAIL.COM

    // Combinando let com Elvis para valor padrão
    val comprimento = email?.let { it.trim().length } ?: 0
    println("Comprimento: $comprimento") // Comprimento: 19

    // Múltiplas verificações com let aninhados
    val nome: String? = "Karina"
    val sobrenome: String? = "Melo"

    nome?.let { n ->
        sobrenome?.let { s ->
            println("Nome completo: $n $s")
        }
    }
    // Nome completo: Karina Melo
}

O padrão variavel?.let { ... } é mais idiomático do que if (variavel != null) quando o objetivo é executar uma ação lateral ou transformar o valor. Para verificações simples, if pode ser mais legível.

Smart Casts

O compilador do Kotlin é inteligente o suficiente para reconhecer verificações de null e automaticamente converter o tipo para non-nullable dentro do bloco correspondente. Isso é chamado de smart cast.

fun processarValor(valor: Any?) {
    // Após verificação com 'is', o compilador faz smart cast
    if (valor is String) {
        // Aqui 'valor' já é tratado como String (non-nullable)
        println("String de ${valor.length} caracteres: ${valor.uppercase()}")
    }

    // Verificação de null também ativa smart cast
    if (valor != null) {
        // Aqui 'valor' é Any (non-nullable)
        println("Valor não-nulo: $valor (tipo: ${valor::class.simpleName})")
    }
}

fun obterComprimento(texto: String?): Int {
    // Smart cast com return antecipado
    if (texto == null) return 0
    // Daqui em diante, 'texto' é tratado como String (non-nullable)
    return texto.length
}

fun classificar(valor: Any?) {
    when (valor) {
        null -> println("Valor nulo")
        is Int -> println("Inteiro: ${valor * 2}")        // smart cast para Int
        is String -> println("Texto: ${valor.uppercase()}") // smart cast para String
        is List<*> -> println("Lista com ${valor.size} itens") // smart cast para List
        else -> println("Outro tipo: $valor")
    }
}

fun main() {
    processarValor("Kotlin Brasil")
    processarValor(42)
    processarValor(null)

    println(obterComprimento("Olá"))  // 3
    println(obterComprimento(null))   // 0

    classificar("teste")
    classificar(42)
    classificar(listOf(1, 2, 3))
    classificar(null)
}

Smart casts funcionam com if, when, && e ||. Eles tornam o código mais limpo, eliminando casts explícitos que seriam necessários em Java. Note que smart casts só funcionam em variáveis locais (val) ou propriedades val sem getter customizado.

Platform Types

Quando você usa código Java a partir do Kotlin, os tipos vêm sem informação de nullability — o Java não distingue entre nullable e non-nullable. O Kotlin trata esses tipos como platform types, representados com ! na documentação (por exemplo, String!).

// Exemplo conceitual: chamando código Java a partir do Kotlin
// Se tivermos uma classe Java:
// public class JavaUtil {
//     public static String getNome() { return null; } // pode retornar null!
// }

// Em Kotlin, o retorno seria String! (platform type)
// val nome = JavaUtil.getNome() // tipo inferido: String!

// RECOMENDAÇÃO: sempre declare o tipo explicitamente ao lidar com código Java
// val nomeSeguro: String? = JavaUtil.getNome() // Seguro: trata como nullable
// val nomePerigoso: String = JavaUtil.getNome() // Perigoso: pode causar NPE

fun main() {
    // Exemplo prático com collections do Java
    val mapa = java.util.HashMap<String, String>()
    mapa["chave"] = "valor"

    // get() retorna String? em Kotlin (tratamento correto)
    val resultado: String? = mapa["inexistente"]
    println(resultado ?: "Não encontrado") // Não encontrado

    // Sempre trate retornos de APIs Java como nullable
    val propriedade: String? = System.getProperty("minha.prop")
    println(propriedade ?: "Propriedade não definida")
}

A regra de ouro ao trabalhar com interoperabilidade Java é: trate tudo como nullable até ter certeza do contrário. Anote seus tipos explicitamente em vez de depender da inferência quando os valores vêm de código Java.

Erros Comuns

Uso excessivo de !!. Cada !! é um ponto potencial de NPE. Se você está usando muitos !! no seu código, provavelmente está ignorando a proposta do null safety. Prefira safe calls e Elvis operator.

Não tratar platform types. Assumir que retornos de APIs Java são non-nullable sem verificação é uma das causas mais frequentes de NPE em projetos Kotlin.

Smart cast em var. Smart casts não funcionam em propriedades var porque outra thread poderia alterar o valor entre a verificação e o uso. Use let ou crie uma variável local val.

var nome: String? = "Kotlin"
// if (nome != null) println(nome.length) // PODE falhar com var
nome?.let { println(it.length) } // Seguro

Ignorar tipos de coleções nullable. List<String> e List<String?> são tipos diferentes. O primeiro garante que nenhum elemento é null; o segundo permite elementos null dentro da lista. List<String>? é uma lista que pode ser null, mas seus elementos não.

Conclusão e Próximos Passos

Neste tutorial, você aprendeu o sistema completo de null safety do Kotlin: tipos nullable, safe calls, Elvis operator, non-null assertion, let/also, smart casts e platform types. Esses mecanismos trabalham juntos para praticamente eliminar NullPointerExceptions do seu código.

O próximo passo é estudar collections em Kotlin, onde você aplicará null safety no contexto de listas, sets e maps. Recomendamos também explorar coroutines onde null safety é essencial para lidar com resultados assíncronos. Com domínio de null safety, você escreve código Kotlin significativamente mais seguro e confiável do que o equivalente em Java.