Segurança de Null em Kotlin: ?, !!, let e Elvis Operator
1. Visão geral
NullPointerException (NPE) é o pesadelo de todo dev Java. Kotlin resolve isso no compile-time com um sistema de null safety integrado ao type system. Este artigo ajuda você a entender quatro ferramentas essenciais: o operador ? para declarar nullable types, safe call ?., elvis operator ?:, e a função let. Depois de ler, você saberá quando usar cada ferramenta e evitará a armadilha clássica: usar !! descontroladamente e ainda assim sofrer com NPE.
2. Nullable Type e o operador ?
Em Kotlin, todo tipo é non-null por padrão. Se você quiser que uma variável possa conter null, deve declarar explicitamente com o operador ? após o tipo.
class Customer(val name: String, val email: String?)
fun main() {
val customer = Customer("An", null)
println(customer.email) // null, sem erro porque email é String?
}
name: String— nunca pode ser null, o compilador garante isso.email: String?— pode ser null, o compilador o obriga a tratar em todo lugar que é usado.
Se você tentar chamar um método diretamente em email como customer.email.length, o compilador reportará erro imediatamente:
error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
Esta é exatamente a maior vantagem: erros são capturados no compile-time, não quando a app está rodando em produção.
3. Safe Call ?.
?. chama um método ou acessa uma propriedade apenas se o receiver for diferente de null. Se o receiver for null, toda a expressão retorna null imediatamente, sem lançar exception.
class Order(val id: Long, val customer: Customer?)
fun printCustomerName(order: Order) {
val length = order.customer?.name?.length
println("Comprimento do nome: $length")
}
order.customer?.name— se customer for null, toda a expressão sofre short-circuit e retorna null, sem precisar executar.length.- O resultado
lengthtem tipoInt?, porque cada passo da cadeia?.pode retornar null.
Comprimento do nome: null // quando customer == null
Comprimento do nome: 2 // quando customer.name = "An"
Nota: Cadeias
?.com mais de 3-4 passos geralmente é sinal de que o design do domain model não está bom. Considere refatorar em funções ou usar sealed class em vez de espalhando nullables.
3.1. Safe Call combinada com chamadas de função
?. também funciona com funções, não apenas propriedades:
fun notifyCustomer(order: Order) {
order.customer?.let { sendEmail(it) }
}
fun sendEmail(customer: Customer) {
println("Enviando email para ${customer.email}")
}
A linha order.customer?.let { ... } executa o bloco let apenas quando customer é diferente de null. Esta parte está diretamente relacionada à seção sobre let na parte 5.
4. Elvis Operator ?:
O Elvis operator fornece um valor padrão quando a expressão à esquerda é null. O nome vem da forma ?: que se parece com o cabelo do Elvis Presley de lado.
fun getDisplayEmail(customer: Customer): String {
return customer.email ?: "email não fornecido"
}
- Se
customer.email != null, retorna o próprio valor. - Se for null, retorna a string de fallback
"email não fornecido".
O Elvis operator é ainda mais poderoso quando combinado com return ou throw, permitindo um early-return limpo:
fun validateOrder(order: Order): Customer {
val customer = order.customer ?: throw IllegalStateException("Pedido ${order.id} sem cliente")
return customer
}
A linha ?: throw IllegalStateException(...) faz o compilador inferir que customer tem tipo non-null Customer depois disso, não sendo mais Customer?. Esta é uma técnica muito comum para “unwrap” nullable cedo na função.
4.1. Elvis com fallback padrão seguro
fun calculateDiscount(order: Order): Double {
val tier = order.customer?.name?.length ?: 0
return if (tier > 5) 0.1 else 0.0
}
Aqui, a cadeia de safe call e elvis se combinam: se qualquer passo retornar null, o resultado final volta para 0.
5. A função let para lidar com valores não-null
let é uma scope function: recebe um receiver, executa um bloco e retorna o resultado do bloco. Combinada com ?., se torna um padrão clássico para executar código apenas quando o valor é diferente de null.
fun processPayment(amount: Double?) {
amount?.let {
println("Processando pagamento: $it")
chargeCard(it)
}
}
fun chargeCard(value: Double) {
println("Debitado $value do cartão")
}
amount?.let { ... }— o bloco dentro executa apenas seamount != null.- Dentro do bloco,
ittem tipoDouble(non-null), nãoDouble?, então você pode chamarchargeCard(it)sem preocupações, sem precisar de!!ou verificações adicionais.
Comparado a escrever if (amount != null) { ... }, usar let ajuda a limitar o escopo da variável temporária, evitando vazamento de variáveis fora do bloco e tornando o código mais parecido com um “pipeline”.
5.1. Combinando let com Elvis para retornar valor
fun formatAmount(amount: Double?): String {
return amount?.let { "Valor: %.2f".format(it) } ?: "Sem valor definido"
}
Esta linha é lida naturalmente: “se houver amount, formate; caso contrário, retorne o fallback”. Este é um idioma muito comum em código Kotlin real, especialmente ao mapear dados de DTO para domain model.
6. Non-null Assertion !! — Quando usar?
!! força o compilador a tratar um tipo nullable como non-null. Se o valor real for null, ele lança NullPointerException imediatamente.
fun getCustomerNameUnsafe(order: Order): String {
return order.customer!!.name
}
order.customer!!— se customer for null, esta linha lança NPE com uma mensagem bem clara:
Exception in thread "main" java.lang.NullPointerException
at MainKt.getCustomerNameUnsafe(Main.kt:2)
Nota:
!!essencialmente traz a NPE do Java de volta para Kotlin manualmente. Use apenas quando tiver 100% de certeza que o valor não é null naquele momento (por exemplo, após validar em uma etapa anterior) e é melhor ter um comentário explicando por quê.
Quando é aceitável usar !!:
- Interop com código Java legado, API retorna
@Nullablemas você tem certeza que a lógica de negócio garante que não é null. - Código de teste, quando você quer falhar rápido se a configuração estiver errada.
Quando não usar !!:
- Lidando com entrada do usuário, requisições HTTP, banco de dados — sempre pode ser null inesperadamente.
- Substituir lógica de validação. Use
requireNotNull()ou elvis + exception com significado mais claro.
fun getCustomerName(order: Order): String {
val customer = requireNotNull(order.customer) { "Pedido ${order.id} deve ter um cliente" }
return customer.name
}
requireNotNull tanto faz o unwrap de nullable quanto fornece uma mensagem de erro muito mais clara do que !! puro — esta é a escolha que deve ser preferida sobre !! direto.
7. Comparação rápida das quatro ferramentas
| Ferramenta | Quando usar | Comportamento ao encontrar null |
|---|---|---|
? |
Declarar um tipo que pode ser null | N/A, apenas uma anotação de tipo |
?. |
Acessar com segurança, quando não tem certeza se é null | Retorna null, não lança exception |
?: |
Precisa de um valor de fallback ou early-return | Executa a expressão à direita |
let |
Executar lógica apenas quando há valor | Não executa o bloco |
!! |
100% seguro que não é null | Lança NullPointerException |
8. Escrevendo testes para funções que lidam com null
Para garantir que a lógica de null-safety funciona corretamente, escreva testes para ambos os casos: com valor e null.
import org.junit.jupiter.api.Test
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
class OrderServiceTest {
@Test
fun `getDisplayEmail retorna fallback quando email null`() {
val customer = Customer(name = "An", email = null)
val result = getDisplayEmail(customer)
assertThat(result).isEqualTo("email não fornecido")
}
@Test
fun `getDisplayEmail retorna email quando tem valor`() {
val customer = Customer(name = "An", email = "an@example.com")
val result = getDisplayEmail(customer)
assertThat(result).isEqualTo("an@example.com")
}
@Test
fun `validateOrder lança quando customer null`() {
val order = Order(id = 1L, customer = null)
assertThatThrownBy { validateOrder(order) }
.isInstanceOf(IllegalStateException::class.java)
.hasMessageContaining("sem cliente")
}
}
- O primeiro teste verifica que elvis retorna fallback correto quando
email == null. - O segundo teste verifica o outro ramo — o valor real não é perdido.
- O último teste verifica se
?: throwlança a exception correta com a mensagem esperada, usandoassertThatThrownBydo AssertJ.
Dependências necessárias para o teste (se usar Gradle Kotlin DSL):
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<scope>test</scope>
</dependency>
9. Erros comuns
Abuso de !! para “calar o compilador”. Quando vê o compilador reportar erro de nullable e reflexivamente digita !!, pare — é sinal de que você não entende bem o fluxo de dados. Pergunte-se: este valor realmente pode ser null? Se puder, trate com ?: ou let.
Usar ?.let no lugar de if-else mecanicamente. Com lógica complexa com muitos ramos, if (x != null) { } else { } às vezes é mais claro do que let aninhado. Não force tudo em elvis/let se o código ficar menos legível.
Esquecer que platform types de Java não têm null safety. Quando chama um método Java que retorna um objeto sem anotação @Nullable/@NonNull, Kotlin o trata como platform type (String!), o compilador não força você a verificar null. NPE ainda pode ocorrer em runtime neste caso — adicione ?. manualmente ou verifique quando trabalhar com Java interop.
// Java: public String getLegacyName() { return null; } // sem anotação
val name = legacyService.getLegacyName() // tipo inferido como String!, Kotlin não avisa
println(name.length) // pode ser NPE em runtime se name for null
10. Conclusão
A null safety de Kotlin não é “magia” que elimina completamente NPE, mas uma ferramenta para capturar erros mais cedo — no compile-time em vez de runtime. ? declara a possibilidade de null, ?. acessa com segurança, ?: fornece fallback ou early-return, let limita o escopo da lógica quando há valor. E !! é uma saída de emergência, use apenas quando tiver certeza e estiver pronto para aceitar uma NPE se estiver errado. Dominando estas quatro ferramentas, você escreverá código Kotlin muito mais seguro do que com os hábitos de null-check style Java antigo.