Cheatsheet Kotlin
Linguagem moderna para Android, backend e multiplataforma
Kotlin
Básico e Tipos
Hello World
fun main() {
println("Olá, Mundo!")
}
// Sem classe obrigatória (diferente de Java)
// main não precisa de args nem tipo de retornoO ponto de entrada é a função main — não precisa de classe envolvente nem de tipo de retorno declarado. O println escreve uma linha no stdout. O compilador infere tudo automaticamente.
Null safety
var nome: String? = null // tipo nullable
val len = nome?.length // safe call (null se null)
val tam = nome?.length ?: 0 // elvis: default se null
val n = nome!!.length // força (NPE se null)
// Executa só se não-null:
nome?.let { println(it) }O sistema de tipos elimina NullPointerException em compilação. O ? marca tipos nullable, o ?. encadeia chamadas com segurança e o ?: (elvis) fornece defaults. O let{} executa blocos só quando o valor existe.
Operadores
// Aritméticos: + - * / % val r = 10 / 3 // 3 (inteiro) // Comparação e lógicos: == != > < >= <= && || ! // Range e pertença: 1..10 // range 5 in 1..10 // true // Atribuição composta: += -= *= /= %= // Não há ++/-- prefixo ambíguo
A divisão inteira trunca. O operador in verifica pertença a ranges e coleções. == compara por valor (como equals em Java); === compara referências. Não há conversões automáticas em expressões mistas.
Variáveis (val e var)
val nome = "Kotlin" // imutável (read-only) var idade = 10 // mutável idade = 11 // OK val pi: Double = 3.14 // tipo explícito // Inferência de tipo: val x = 42 // Int val s = "texto" // String
Use val por defeito (imutável) para evitar bugs; só use var quando o valor precisar de mudar. O tipo é inferido a partir do valor, mas pode ser anotado explicitamente com : Tipo.
Arrays
val nums = arrayOf(1, 2, 3, 4, 5)
val zeros = IntArray(5) { 0 }
val pares = Array(5) { i -> i * 2 }
println(nums[0]) // 1
println(nums.size) // 5
nums[0] = 10 // mutável
// Iterar:
for (n in nums) println(n)Arrays têm tamanho fixo após criação. IntArray/DoubleArray são versões otimizadas sem boxing. O lambda no construtor inicializa cada posição. Para coleções dinâmicas prefira List/MutableList.
lateinit e lazy
// lateinit: inicialização tardia (var)
class Activity {
lateinit var db: Database
fun onCreate() {
db = Database.abrir()
}
}
// lazy: calcula no 1.º acesso (val)
val config: Config by lazy {
Config.carregarDoFicheiro()
}lateinit declara propriedades não-nulas inicializadas depois (ex: injeção de dependências) — aceder antes lança exceção. O by lazy adia o cálculo de um val até ao primeiro acesso (thread-safe por defeito).
Tipos de dados
val inteiro: Int = 42 val longo: Long = 100_000_000L val decimal: Double = 3.14 val flag: Boolean = true val letra: Char = 'A' val texto: String = "Kotlin" val nada: Unit = Unit // Underscore para legibilidade: val milhao = 1_000_000
Kotlin trata os primitivos como objetos (sem distinção int/Integer como em Java). O underscore em números melhora a legibilidade. O Unit equivale ao void do Java mas é um tipo real.
Type aliases
typealias UserId = Long
typealias Callback = (String) -> Unit
typealias Tabela = Map<String, List<Any>>
fun registar(id: UserId, cb: Callback) {
cb("utilizador $id registado")
}
registar(42L) { println(it) }typealias cria nomes legíveis para tipos complexos — ótimo para callbacks, IDs e mapas aninhados. Não cria um tipo novo (é só um atalho), mas melhora muito a legibilidade das assinaturas.
Nothing e Any
// Any: super-tipo de todos
val obj: Any = 42
val obj2: Any = "texto"
// Nothing: função nunca retorna
fun falhar(msg: String): Nothing {
throw IllegalStateException(msg)
}
// Útil em expressões:
val x = valor ?: falhar("sem valor")
// aqui x é não-nullAny é o super-tipo de todos os tipos (como Object em Java). Nothing indica que uma função nunca retorna normalmente (lança exceção ou loop infinito) — ajuda o compilador a inferir nullability após ?:.
Strings e templates
val nome = "Mundo"
val saudacao = "Olá, $nome!"
val template = "2 + 2 = ${2 + 2}"
// Multilinha (triple-quoted):
val multi = """
|Linha 1
|Linha 2
""".trimMargin()
// Sem escape de aspas:
val json = """{"nome": "$nome"}"""String templates com $ evitam concatenação; para expressões use ${}. Triple-quoted strings (\"\"\") preservam formatação e são ideais para SQL/JSON — o trimMargin() remove a indentação.
Conversões de tipos
val x = 42 val s = x.toString() // Int -> String val n = "123".toInt() // String -> Int val d = x.toDouble() // Int -> Double val l = x.toLong() // Int -> Long // Sem conversões implícitas: // val d: Double = x // ERRO! val d2: Double = x.toDouble() // toIntOrNull (seguro): val inv = "abc".toIntOrNull() // null
Kotlin não faz conversões numéricas implícitas — use toInt(), toDouble(), etc. explicitamente. O toIntOrNull() retorna null em vez de lançar exceção se a string não for um número válido.
Controlo de Fluxo
If como expressão
// if retorna valor:
val max = if (a > b) a else b
// Com blocos:
val msg = if (nota >= 10) {
println("Aprovado")
"OK"
} else {
"Reprovado"
}
// Não existe operador ternário ?:
// Use if-else como expressãoEm Kotlin o if é uma expressão que retorna um valor — substitui o operador ternário (que não existe). O último statement de cada bloco é o valor retornado. Torna o código mais declarativo.
While e do-while
var n = 10
while (n > 0) {
n--
}
// do-while executa pelo menos 1x:
do {
val input = readLine()
println("Lido: $input")
} while (input != "sair")O while verifica a condição antes de executar (pode nunca executar). O do-while executa pelo menos uma vez — ideal para menus e validação de input onde precisa de ler antes de verificar.
Smart casts
fun processar(obj: Any) {
if (obj is String) {
// obj já é String aqui:
println(obj.length)
println(obj.uppercase())
}
if (obj !is List<*>) return
// obj é List a partir daqui:
println(obj.size)
}O compilador faz smart cast automático após verificações is/!is — dentro do bloco, a variável é tratada como o tipo testado sem cast manual. Só funciona com val locais ou propriedades imutáveis.
When
val resultado = when (x) {
1 -> "um"
2, 3 -> "dois ou três"
in 4..10 -> "entre 4 e 10"
!in 20..30 -> "fora de 20-30"
else -> "outro"
}
// when é expressão (retorna valor)
// else é obrigatório se não for exaustivoO when substitui o switch do Java com padrões mais poderosos: múltiplos valores separados por vírgula, ranges com in, negação com !in. É uma expressão que retorna valor.
Break, continue e labels
for (i in 1..10) {
if (i == 3) continue // salta o 3
if (i == 7) break // termina no 7
print(i) // 1 2 4 5 6
}
// Labels para ciclos aninhados:
outer@ for (i in 1..3) {
for (j in 1..3) {
if (i * j > 4) break@outer
}
}continue salta para a próxima iteração, break termina o ciclo. Labels (outer@) permitem controlar ciclos aninhados — break@outer sai diretamente do ciclo exterior.
return, break em lambdas
fun procurar(lista: List<Int>) {
lista.forEach {
if (it == 3) return // retorna da função!
}
}
// Label para retornar só do lambda:
lista.forEach loop@{
if (it == 3) return@loop // só deste lambda
println(it)
}
// Nome implícito do lambda:
lista.forEach {
if (it > 5) return@forEach
}Um return dentro de um lambda retorna da função envolvente (non-local return). Para retornar só do lambda use labels (return@forEach). Isto é essencial para controlar o fluxo em funções de ordem superior.
When avançado
// Sem argumento (if-else chain):
val msg = when {
x < 0 -> "negativo"
x == 0 -> "zero"
else -> "positivo"
}
// Com tipos (smart cast):
fun tipo(obj: Any) = when (obj) {
is String -> "Texto: ${obj.length}"
is Int -> "Número: $obj"
is List<*> -> "Lista: ${obj.size}"
else -> "?"
}O when sem argumento funciona como cadeia if-else mais legível. Com is, o Kotlin faz smart cast automático — dentro do branch, obj já é tratado como o tipo testado sem cast manual.
Ranges e progressões
val r1 = 1..10 // IntRange (inclusivo)
val r2 = 1 until 10 // 1..9 (exclui último)
val r3 = 'a'..'z' // CharRange
val r4 = 10 downTo 1 // decrescente
if (5 in r1) println("contém")
for (c in 'a'..'f') print(c) // abcdef
val lista = (1..5).toList() // [1,2,3,4,5]Ranges são objetos Iterable que suportam iteração e verificação de pertença (in). until exclui o último elemento (útil para índices). Funcionam com Int, Long e Char.
Ciclos for e ranges
for (i in 1..5) print(i) // 1 2 3 4 5
for (i in 5 downTo 1) print(i) // 5 4 3 2 1
for (i in 0..10 step 2) print(i) // 0 2 4 6 8 10
for (i in 1 until 5) print(i) // 1 2 3 4
// Com índice e valor:
for ((i, v) in lista.withIndex()) {
println("$i: $v")
}Ranges (..) criam sequências inclusivas. downTo inverte a direção, step define o incremento e until exclui o último. O withIndex() dá acesso ao índice e valor simultaneamente.
Expressões (try/when)
// try como expressão:
val n = try {
"42".toInt()
} catch (e: NumberFormatException) {
0
}
// when como expressão:
val code = when (status) {
"ok" -> 200
else -> 500
}
// if como expressão:
val max = if (a > b) a else bDiferente de Java, if/when/try são expressões que retornam valores em Kotlin. Isto elimina variáveis temporárias e torna o código mais declarativo. O último statement de cada bloco é o valor.
Funções e Lambdas
Declaração de funções
fun soma(a: Int, b: Int): Int {
return a + b
}
// Corpo de expressão (retorno inferido):
fun dobro(x: Int) = x * 2
// Sem retorno útil (Unit):
fun saudar(nome: String) {
println("Olá, $nome!")
}Funções com corpo de expressão (=) são mais concisas e o tipo de retorno é inferido. O Unit (sem retorno) pode ser omitido. Os tipos dos parâmetros são sempre obrigatórios.
Extension functions
fun String.capitalize(): String =
replaceFirstChar { it.uppercase() }
fun Int.isEven(): Boolean = this % 2 == 0
fun List<Int>.segundo(): Int? =
if (size >= 2) this[1] else null
// Uso:
"kotlin".capitalize() // "Kotlin"
4.isEven() // true
listOf(1, 2).segundo() // 2Extension functions adicionam métodos a classes existentes sem herança nem modificação do código fonte. São resolvidas estaticamente em compilação. Ideais para utilitários e APIs mais expressivas via fun Tipo.metodo().
Operator overloading
data class Vec(val x: Int, val y: Int)
operator fun Vec.plus(o: Vec) =
Vec(x + o.x, y + o.y)
operator fun Vec.times(n: Int) =
Vec(x * n, y * n)
val v = Vec(1, 2) + Vec(3, 4) // Vec(4, 6)
val d = Vec(1, 2) * 3 // Vec(3, 6)Operator overloading permite usar +, -, *, [] com tipos customizados. Basta marcar a função com operator e usar o nome convencionado (plus, times, get).
Default e named args
fun greet(nome: String, prefixo: String = "Olá") =
"$prefixo, $nome!"
greet("Ana") // "Olá, Ana!"
greet("Ana", "Bom dia") // "Bom dia, Ana!"
// Named arguments:
fun config(host: String, port: Int = 80, ssl: Boolean = false) {}
config("localhost", ssl = true) // port default 80Parâmetros com default eliminam sobrecargas — a mesma função serve para várias chamadas. Named args melhoram a legibilidade e permitem saltar parâmetros com default, especificando só os necessários.
Higher-order functions
// Recebe função:
fun operacao(a: Int, b: Int, op: (Int, Int) -> Int): Int {
return op(a, b)
}
operacao(3, 4) { x, y -> x + y } // 7
// Retorna função:
fun multiplicador(fator: Int): (Int) -> Int =
{ n -> n * fator }
val triplo = multiplicador(3)
triplo(5) // 15Higher-order functions recebem ou retornam outras funções — a base da programação funcional. O tipo (Int, Int) -> Int descreve a assinatura. Permitem criar factories, estratégias e composição de comportamento.
Varargs e spread
// vararg: N argumentos do mesmo tipo
fun soma(vararg nums: Int) = nums.sum()
soma(1, 2, 3, 4, 5) // 15
// Spread operator (*):
val arr = intArrayOf(1, 2, 3)
soma(*arr) // 6
// vararg com outros parâmetros:
fun log(nivel: String, vararg msgs: String) {
msgs.forEach { println("[$nivel] $it") }
}
log("INFO", "arrancou", "pronto")vararg aceita N argumentos do mesmo tipo (internamente é Array). O spread operator (*) passa um array como vararg. Pode combinar com parâmetros normais usando named args.
Lambdas
val soma = { a: Int, b: Int -> a + b }
val dobro: (Int) -> Int = { it * 2 }
val nada: () -> Unit = { println("oi") }
// it: nome implícito (1 parâmetro)
listOf(1, 2, 3).forEach { println(it) }
// Múltiplos parâmetros:
val mult = { x: Int, y: Int -> x * y }
mult(3, 4) // 12Lambdas são funções anónimas de primeira classe. it é o nome implícito quando há um só parâmetro. O tipo (Int) -> Int descreve a assinatura. São a base das operações de coleções e DSLs.
Scope functions
// let: transforma + null-safety
val len = nome?.let { it.trim().length }
// apply: configura (retorna o próprio)
val p = Pessoa().apply { nome = "Ana"; idade = 25 }
// run: bloco com resultado
val info = p.run { "$nome tem $idade" }
// also: efeito secundário
lista.also { println("Tam: ${it.size}") }
// with: operações no objeto
with(p) { println(nome); println(idade) }Scope functions eliminam variáveis temporárias. let para transformações null-safe, apply para configuração/builders, run para computação, also para efeitos secundários e with para múltiplas operações no mesmo objeto.
Funções infix e suspend
// infix: chamada sem ponto nem parênteses
infix fun Int.mais(x: Int) = this + x
val r = 5 mais 3 // 8
// "to" é infix (cria Pair):
val par = "a" to 1 // Pair("a", 1)
// suspend: função de coroutine
suspend fun fetchDados(): String {
delay(1000)
return "dados"
}Funções infix (1 parâmetro, membro ou extensão) podem ser chamadas sem ponto nem parênteses — to é o exemplo mais comum. suspend marca funções que podem ser suspensas em coroutines (só chamáveis de outra suspend ou coroutine).
Trailing lambda
// Lambda como último argumento:
listOf(1, 2, 3).forEach({ println(it) })
// Trailing lambda (fora dos parênteses):
listOf(1, 2, 3).forEach { println(it) }
// Se for o único argumento:
listOf(1, 2, 3).map { it * 2 }
// Vários lambdas:
fun build(init: Builder.() -> Unit, validate: () -> Boolean) {}
build({ /* config */ }) { /* valida */ }Quando o lambda é o último argumento, pode ir fora dos parênteses (trailing lambda) — essencial para APIs fluentes e DSLs. Se for o único argumento, os parênteses podem ser omitidos por completo.
Inline functions
// inline copia o lambda para o call site:
inline fun medirTempo(bloco: () -> Unit) {
val inicio = System.currentTimeMillis()
bloco()
println("${System.currentTimeMillis() - inicio}ms")
}
// noinline: não inline este lambda
inline fun foo(a: () -> Unit, noinline b: () -> Unit) {}
// crossinline: proíbe non-local return
inline fun bar(crossinline b: () -> Unit) {}inline copia o corpo do lambda para o call site, eliminando alocação de objetos — crucial em loops. noinline exclui um lambda específico; crossinline proíbe non-local returns quando o lambda é chamado noutro contexto.
Classes e Objetos
Classes e construtores
class Pessoa(val nome: String, var idade: Int) {
var email: String = ""
init {
require(idade >= 0) { "Idade inválida" }
}
// Construtor secundário:
constructor(nome: String) : this(nome, 0)
}
val p = Pessoa("Ana", 25)O primary constructor na declaração da classe é conciso — val/var criam propriedades automaticamente. O bloco init valida na criação. Construtores secundários delegam no primário com this().
Interfaces
interface Nadador {
fun nadar(): String
// Método com implementação default:
fun flutuar(): String = "a flutuar"
}
interface Voador {
fun voar(): String
}
class Pato : Nadador, Voador {
override fun nadar() = "a nadar"
override fun voar() = "a voar"
}Interfaces podem ter métodos abstratos e métodos com implementação default. Uma classe pode implementar múltiplas interfaces (mas herdar de uma só classe). Se houver conflito de defaults, use super<Interface>.
Companion object
class User private constructor(val nome: String) {
companion object Factory {
fun create(json: String): User = User(json)
fun guest() = User("Convidado")
const val MAX = 100
}
}
// Acesso via nome da classe:
val u = User.create("{...}")
val g = User.guest()
println(User.MAX)companion object substitui membros static do Java — acede-se via ClassName.membro(). Ideal para factories, constantes (const val) e métodos utilitários. Pode ter nome ou ser anónimo.
Propriedades
class Retangulo(val largura: Int, val altura: Int) {
// Propriedade calculada:
val area: Int
get() = largura * altura
// Com setter customizado:
var nome: String = ""
set(value) {
field = value.trim()
}
}
val r = Retangulo(3, 4)
println(r.area) // 12 (calculado)Propriedades podem ter get() customizado (calculadas) e set() com lógica. O field é a backing field (o valor real). Não há métodos getter/setter boilerplate como em Java — tudo é propriedade.
Sealed classes
sealed class Resultado<out T> {
data class Sucesso<T>(val dados: T) : Resultado<T>()
data class Erro(val msg: String) : Resultado<Nothing>()
object ACarregar : Resultado<Nothing>()
}
fun tratar(r: Resultado<String>) = when (r) {
is Resultado.Sucesso -> "Dados: ${r.dados}"
is Resultado.Erro -> "Erro: ${r.msg}"
Resultado.ACarregar -> "..."
} // exaustivo — sem elsesealed class restringe a hierarquia a um conjunto fixo de subclasses. O compilador verifica exaustividade no when — se adicionar um novo caso, todos os when quebram em compilação. Ideal para estados e resultados.
Delegation (by)
interface Printer { fun print(msg: String) }
class ConsolePrinter : Printer {
override fun print(msg: String) = println(msg)
}
// Delega toda a interface:
class Logger(printer: Printer) : Printer by printer
val log = Logger(ConsolePrinter())
log.print("olá") // imprime no consoleA delegation (by) implementa o padrão de delegação sem boilerplate — a classe encaminha todas as chamadas da interface para o objeto delegado. Pode sobrescrever métodos específicos se necessário.
Data classes
data class User(val nome: String, val email: String, val idade: Int)
val u1 = User("Ana", "ana@mail.com", 25)
val u2 = u1.copy(nome = "Maria") // novo objeto
println(u1) // User(nome=Ana, ...)
println(u1 == u2) // false (compara valores)
// Destructuring:
val (nome, email) = u1data class gera automaticamente equals(), hashCode(), toString(), copy() e componentN(). Ideais para modelos, DTOs e estados. O copy() "modifica" imutáveis criando uma cópia com campos alterados.
Enum classes
enum class Estado(val cor: String) {
ATIVO("#00FF00"),
INATIVO("#FF0000"),
PENDENTE("#FFAA00");
fun descricao() = "Estado: $name ($cor)"
}
val e = Estado.ATIVO
println(e.cor) // #00FF00
println(e.ordinal) // 0
val todos = Estado.values()Enums em Kotlin são classes completas — podem ter propriedades, métodos e implementar interfaces. Cada constante é uma instância acedida por Estado.ATIVO. Úteis para estados e conjuntos fixos; values() lista todos.
Nested e inner classes
class Exterior {
val x = 10
// Nested (sem referência ao exterior):
class Nested {
fun ola() = "nested"
}
// Inner (acede ao exterior):
inner class Inner {
fun ola() = "x = $x"
}
}
val n = Exterior.Nested()
val i = Exterior().Inner()Classes aninhadas (nested) não têm referência à instância exterior (como static em Java). Classes inner mantêm a referência e acedem aos membros do exterior. Use inner só quando realmente precisa do contexto externo.
Herança (open)
open class Animal(val nome: String) {
open fun som(): String = "..."
}
class Pato(nome: String) : Animal(nome) {
override fun som() = "Quack!"
}
// super:
class Cao(nome: String) : Animal(nome) {
override fun som() = "Au! " + super.som()
}Classes em Kotlin são final por defeito — use open para permitir herança. Métodos também precisam de open para serem sobrescritos com override. O super chama a implementação da superclasse.
Object (singleton)
// Singleton thread-safe:
object AppConfig {
val apiUrl = "https://api.exemplo.com"
fun getVersion() = "1.0.0"
}
// Uso:
println(AppConfig.apiUrl)
// Object expression (anónimo):
val listener = object : OnClickListener {
override fun onClick() = println("clicado")
}object cria singletons thread-safe com lazy initialization (uma única instância). Substitui classes utilitárias com métodos estáticos. Object expressions criam instâncias anónimas de interfaces (como classes anónimas em Java).
Coleções
Listas
val imutavel = listOf(1, 2, 3, 4, 5) val mutavel = mutableListOf(1, 2, 3) mutavel.add(4) mutavel.removeAt(0) mutavel[0] = 10 println(imutavel.first()) // 1 println(imutavel.last()) // 5 println(imutavel.size) // 5
listOf() cria listas imutáveis (read-only) — a API não expõe métodos de modificação. mutableListOf() permite add/remove/set. Prefira imutáveis por defeito para evitar efeitos secundários.
Ordenação
val nums = listOf(5, 2, 8, 1, 9)
val asc = nums.sorted() // [1,2,5,8,9]
val desc = nums.sortedDescending() // [9,8,5,2,1]
// Por propriedade:
val porIdade = pessoas.sortedBy { it.idade }
val porNome = pessoas.sortedByDescending { it.nome }
// Comparador customizado:
val custom = nums.sortedWith(compareBy({ it % 2 }, { it }))sorted() retorna nova lista ordenada (não modifica a original). sortedBy ordena por uma propriedade extraída. sortedWith aceita um Comparator customizado. compareBy compõe múltiplos critérios.
flatMap e flatten
val listas = listOf(listOf(1, 2), listOf(3, 4), listOf(5))
val plana = listas.flatten() // [1,2,3,4,5]
val flat = listas.flatMap { it } // [1,2,3,4,5]
// Transformar e achatar:
val palavras = frases.flatMap { it.split(" ") }
// Palavras únicas de todas as frases:
val unicas = frases
.flatMap { it.split(" ") }
.distinct()flatMap transforma cada elemento numa coleção e achata tudo numa única lista. flatten() achata listas aninhadas diretamente. É ideal para extrair e combinar elementos de estruturas nested.
Map e Set
val mapa = mapOf("a" to 1, "b" to 2, "c" to 3)
println(mapa["a"]) // 1
println(mapa.getOrDefault("z", 0)) // 0
val mutMap = mutableMapOf<String, Int>()
mutMap["d"] = 4
val set = setOf(1, 2, 2, 3, 3) // {1, 2, 3}
println(3 in set) // trueMap armazena pares chave-valor com acesso O(1) por chave. Set garante elementos únicos — duplicados são ignorados. Ambos têm versões mutáveis. O to cria Pair usado na construção de mapas.
Pesquisa
val nums = listOf(5, 2, 8, 1, 9)
val achado = nums.find { it > 7 } // 8 (primeiro)
val ultimo = nums.findLast { it < 5 } // 1
val indice = nums.indexOf(8) // 2
val existe = nums.any { it > 8 } // true
val todos = nums.all { it > 0 } // true
val nenhum = nums.none { it < 0 } // true
val conta = nums.count { it % 2 == 0 } // 2find retorna o primeiro elemento que satisfaz o predicado (ou null). any/all/none verificam condições booleanas. count conta os que passam. indexOf dá a posição de um valor.
distinct, chunked e zip
val unicos = listOf(1, 1, 2, 3, 3).distinct() // [1,2,3]
val grupos = (1..7).chunked(3)
// [[1,2,3], [4,5,6], [7]]
val a = listOf("a", "b", "c")
val b = listOf(1, 2, 3)
val pares = a.zip(b) // [(a,1), (b,2), (c,3)]
val (x, y) = listOf(1, 2, 3).partition { it % 2 == 0 }
// x=[2], y=[1,3]distinct() remove duplicados. chunked(n) divide em sublistas de tamanho fixo (útil para paginação). zip combina duas listas em pares. partition divide em duas listas (passam/não passam no predicado).
map e filter
val nums = listOf(1, 2, 3, 4, 5, 6)
val dobros = nums.map { it * 2 }
// [2, 4, 6, 8, 10, 12]
val pares = nums.filter { it % 2 == 0 }
// [2, 4, 6]
val nomes = pessoas.map { it.nome }
val maiores = pessoas.filter { it.idade >= 18 }map transforma cada elemento mantendo o tamanho; filter seleciona os que passam no predicado. Ambas retornam novas listas (não modificam a original). São as operações mais usadas em coleções.
Agrupamento
val pessoas = listOf(
Pessoa("Ana", 25), Pessoa("Rui", 30), Pessoa("Mia", 25)
)
val porIdade = pessoas.groupBy { it.idade }
// {25=[Ana,Mia], 30=[Rui]}
val nomes = pessoas.associateBy { it.nome }
// {"Ana"=Pessoa(...), ...}
val pares = pessoas.associate { it.nome to it.idade }
// {"Ana"=25, "Rui"=30, "Mia"=25}groupBy agrupa elementos por chave num Map<K, List<V>>. associateBy cria um Map indexado por chave. associate constrói pares chave-valor arbitrários. Essenciais para transformar listas em mapas.
joinToString e operações de texto
val nums = listOf(1, 2, 3, 4, 5)
val csv = nums.joinToString(", ")
// "1, 2, 3, 4, 5"
val custom = nums.joinToString(
separator = " | ",
prefix = "[",
postfix = "]"
) { it.toString() }
// "[1 | 2 | 3 | 4 | 5]"
val palavras = "a b c".split(" ") // [a, b, c]
val junto = palavras.joinToString("") // "abc"joinToString() converte coleções em string com separador, prefixo e postfix customizáveis, e um transformador por elemento. split() faz o inverso. São ideais para construir CSVs, logs e output formatado.
reduce, fold e sum
val nums = listOf(1, 2, 3, 4, 5)
val soma = nums.reduce { acc, n -> acc + n } // 15
val total = nums.sum() // 15
// fold: com valor inicial (tipos diferentes):
val texto = nums.fold("") { acc, n -> "$acc$n " }
val produto = nums.fold(1) { acc, n -> acc * n } // 120reduce acumula todos os elementos num único valor (o primeiro é o inicial). fold aceita um valor inicial explícito e permite tipos diferentes entre acumulador e elementos. sum() é um atalho para somar números.
Sequences (lazy)
// Eager: cria listas intermédias
val r1 = lista.filter { it > 5 }.map { it * 2 }.take(3)
// Lazy: processa elemento a elemento
val r2 = lista.asSequence()
.filter { it > 5 }
.map { it * 2 }
.take(3)
.toList() // só aqui processa
// Infinita:
val naturais = generateSequence(1) { it + 1 }
val primeiros = naturais.take(10).toList()Sequences processam elemento a elemento (lazy) em vez de criar listas intermédias a cada operação. Essencial para coleções grandes ou infinitas — só computa o necessário quando a operação terminal (toList()) é chamada.
Avançado
Coroutines básico
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000) // suspende sem bloquear
println("Mundo")
}
println("Olá")
}
// Olá (imediato) -> Mundo (após 1s)Coroutines são funções suspendíveis que permitem concorrência sem threads bloqueadas. launch inicia uma coroutine fire-and-forget. delay suspende sem bloquear a thread — ideal para I/O, timers e rede.
Exceções em coroutines
val handler = CoroutineExceptionHandler { _, e ->
println("Erro: ${e.message}")
}
CoroutineScope(Dispatchers.Default).launch(handler) {
throw RuntimeException("falhou")
}
// try/catch com async:
val r = async { operacao() }
try {
r.await()
} catch (e: Exception) {
println("erro: $e")
}Erros em launch propagam para o handler de exceção (CoroutineExceptionHandler). Em async, a exceção é relançada no await() — use try/catch. Um CoroutineScope cancela os filhos em caso de falha (structured concurrency).
Reflection
import kotlin.reflect.full.*
class Pessoa(val nome: String, val idade: Int)
val p = Pessoa("Ana", 25)
val klass = p::class
println(klass.simpleName) // "Pessoa"
klass.memberProperties.forEach {
println("${it.name} = ${it.get(p)}")
}
// Function reference:
val fn = ::println
fn("olá")Reflection (com kotlin-reflect) inspeciona classes, propriedades e funções em runtime. ::class obtém o KClass, memberProperties lista as propriedades. Function references (::println) passam funções como valores.
async / await
fun main() = runBlocking {
val r1 = async { fetchDados1() }
val r2 = async { fetchDados2() }
// Espera ambos em paralelo:
val total = r1.await() + r2.await()
println("Total: $total")
}
suspend fun fetchDados1(): Int {
delay(1000); return 42
}async inicia uma coroutine que retorna um Deferred (future). await() suspende até o resultado estar disponível. Múltiplos async executam em paralelo — ideal para chamadas de rede concorrentes que reduzem o tempo total.
DSL building
class HtmlBuilder {
val elements = mutableListOf<String>()
fun div(c: String) { elements.add("<div>$c</div>") }
fun p(c: String) { elements.add("<p>$c</p>") }
}
fun html(block: HtmlBuilder.() -> Unit): String {
val b = HtmlBuilder().apply(block)
return b.elements.joinToString("\n")
}
val page = html {
div("Título")
p("Conteúdo")
}DSLs em Kotlin usam extension lambdas (Receiver.() -> Unit) para criar APIs declarativas. O objeto recetor torna-se o this implícito dentro do bloco. Usado em Gradle, Ktor, Exposed e Jetpack Compose.
Structured concurrency
fun main() = runBlocking {
// coroutineScope: espera todos os filhos
coroutineScope {
launch { tarefa1() }
launch { tarefa2() }
} // só continua quando ambas terminam
// supervisorScope: falha não cancela irmãos
supervisorScope {
val a = launch { throw Exception() }
val b = launch { tarefa2() } // continua
}
}Structured concurrency garante que coroutines têm ciclo de vida definido. coroutineScope espera todos os filhos e propaga falhas. supervisorScope isola falhas — um filho falhar não cancela os irmãos. Essencial para código robusto.
Dispatchers
import kotlinx.coroutines.*
fun main() = runBlocking {
// CPU-bound:
val r = withContext(Dispatchers.Default) {
calculoPesado()
}
// I/O (rede, disco):
val dados = withContext(Dispatchers.IO) {
lerFicheiro()
}
// UI (Android):
withContext(Dispatchers.Main) { atualizarUi() }
}Dispatchers definem em que thread pool a coroutine corre. Dispatchers.Default para CPU-bound, Dispatchers.IO para I/O (rede/disco), Dispatchers.Main para a UI (Android). withContext muda de dispatcher sem criar nova coroutine.
Delegates (lazy/observable)
// lazy: calcula 1x no primeiro acesso
val config: Config by lazy {
Config.carregarDoFicheiro()
}
// observable: notifica mudanças
var nome: String by Delegates.observable("") { _, old, new ->
println("$old -> $new")
}
// vetoable: pode rejeitar
var idade: Int by Delegates.vetoable(0) { _, _, new ->
new >= 0
}by lazy adia computação cara até ao primeiro acesso (singleton thread-safe). observable notifica após cada mudança. vetoable permite rejeitar valores inválidos antes de aplicar. Todos eliminam boilerplate de getter/setter.
Channel (comunicação)
import kotlinx.coroutines.channels.Channel
fun main() = runBlocking {
val channel = Channel<Int>()
launch {
for (i in 1..5) channel.send(i * i)
channel.close()
}
for (valor in channel) {
println(valor) // 1, 4, 9, 16, 25
}
}Channel permite comunicação entre coroutines (produtor/consumidor). send() envia (suspende se cheio), receive() recebe (suspende se vazio). close() termina o canal. Iterar em channel recebe até fechar.
Flow (reativo)
import kotlinx.coroutines.flow.*
fun numeros(): Flow<Int> = flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}
runBlocking {
numeros()
.filter { it % 2 == 0 }
.map { it * 10 }
.collect { println(it) } // 20, 40
}Flow é a API reativa de Coroutines — emite múltiplos valores assincronamente. É cold (só executa quando collect é chamado). Suporta operadores como filter, map, take — similar a RxJava mas integrado.
Annotations
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Log(val nivel: String = "INFO")
@Log(nivel = "DEBUG")
fun operacao() { }
// Annotations da stdlib:
@Deprecated("use novo()", ReplaceWith("novo()"))
fun antigo() {}
@JvmStatic
fun utilitario() {}Annotations adicionam metadados ao código. @Target define onde se aplica, @Retention se está disponível em runtime. A stdlib tem @Deprecated (com sugestão de substituição) e @JvmStatic/@JvmOverloads para interop com Java.
OO e Genéricos
Visibility modifiers
class Exemplo {
public val a = 1 // público (default)
private val b = 2 // só nesta classe
protected val c = 3 // classe + subclasses
internal val d = 4 // só neste módulo
}
// Top-level:
private fun helper() {} // só neste ficheiro
internal val config = 1 // só neste móduloKotlin tem quatro modificadores: public (default), private (classe/ficheiro), protected (classe + subclasses) e internal (visível no módulo). Tudo é público por defeito — restrinja com private o máximo possível.
reified e inline
// reified mantém o tipo em runtime:
inline fun <reified T> tipoDe(x: Any): Boolean =
x is T
tipoDe<String>("olá") // true
tipoDe<Int>("olá") // false
// Uso prático:
inline fun <reified T> Gson.fromJson(json: String): T =
fromJson(json, T::class.java)Normalmente os tipos genéricos são apagados em runtime (type erasure). reified (só com inline) preserva o tipo, permitindo verificações is T e obter T::class. Muito usado em serialização e reflection.
Value classes (inline)
@JvmInline
value class Email(val valor: String) {
init { require("@" in valor) { "email inválido" } }
fun dominio() = valor.substringAfter("@")
}
val e = Email("ana@mail.com")
println(e.dominio()) // mail.com
// Em runtime é só String (sem overhead de objeto)value class (inline class) envolve um único valor sem overhead de objeto em runtime — type safety de um tipo distinto com performance de um primitivo. Ideal para IDs, emails e newtypes com validação no init.
Abstract classes
abstract class Forma {
abstract fun area(): Double
// Método concreto:
fun descrever() = "Área: ${area()}"
}
class Circulo(val raio: Double) : Forma() {
override fun area() = Math.PI * raio * raio
}
// Não pode instanciar:
// val f = Forma() // ERROClasses abstract não podem ser instanciadas e podem ter métodos abstratos (sem corpo) que as subclasses implementam. Combinam herança com contrato — úteis quando há código partilhado mas comportamento específico por subtipo.
Type checks e casts
fun processar(obj: Any) {
if (obj is String) {
println(obj.uppercase()) // smart cast
}
// Cast explícito:
val s = obj as? String // seguro (null se falhar)
val n = obj as Int // lança ClassCastException
// Star projection:
fun tamanho(lista: List<*>) = lista.size
}is verifica o tipo com smart cast automático. as? é o cast seguro (retorna null se falhar); as lança exceção. List<*> (star projection) aceita listas de qualquer tipo quando não precisa do tipo concreto.
Builder pattern
class PedidoBuilder {
var item: String = ""
var quantidade: Int = 1
var urgente: Boolean = false
fun build() = Pedido(item, quantidade, urgente)
}
fun pedido(block: PedidoBuilder.() -> Unit): Pedido =
PedidoBuilder().apply(block).build()
val p = pedido {
item = "Livro"
quantidade = 2
urgente = true
}O builder pattern em Kotlin usa extension lambda (Builder.() -> Unit) com apply — cria uma DSL declarativa para configurar objetos. Mais legível que construtores com muitos parâmetros opcionais.
Generics
// Classe genérica:
class Box<T>(val value: T)
val intBox = Box(42) // Box<Int> inferido
val strBox = Box("texto")
// Função genérica:
fun <T> primeiro(lista: List<T>): T? =
lista.firstOrNull()
// Constraint:
fun <T : Comparable<T>> maior(a: T, b: T): T =
if (a > b) a else bGenerics permitem código reutilizável com type safety. O compilador infere tipos na maioria dos casos. Constraints (T : Comparable<T>) restringem os tipos aceites a quem implementa uma interface.
equals e hashCode
class Ponto(val x: Int, val y: Int) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Ponto) return false
return x == other.x && y == other.y
}
override fun hashCode(): Int = 31 * x + y
}
// data class gera isto automaticamente!
data class Ponto2(val x: Int, val y: Int)Sobrescreva equals() e hashCode() juntos para comparação por valor (necessário em Set/Map). === compara referências. Prefira data class que gera ambos automaticamente.
Variância (in/out)
// out (covariância): só produz T
class Produtor<out T>(val item: T) {
fun obter(): T = item
}
val p: Produtor<Any> = Produtor("texto") // OK
// in (contravariância): só consome T
class Consumidor<in T> {
fun consumir(item: T) { println(item) }
}
val c: Consumidor<String> = Consumidor<Any>() // OKout (covariância) significa que a classe só produz T — Produtor<String> é um Produtor<Any>. in (contravariância) só consome T. Permite subtipagem segura em genéricos (PECS: Producer Extends, Consumer Super).
Sealed interfaces
sealed interface Evento {
data class Clique(val x: Int, val y: Int) : Evento
data class Tecla(val codigo: Int) : Evento
object Fecho : Evento
}
fun tratar(e: Evento) = when (e) {
is Evento.Clique -> "clique em ${e.x},${e.y}"
is Evento.Tecla -> "tecla ${e.codigo}"
Evento.Fecho -> "fecho"
} // exaustivosealed interface (Kotlin 1.5+) combina interfaces com hierarquia fechada — as implementações são conhecidas em compilação, permitindo when exaustivo. Mais flexível que sealed class porque uma classe pode implementar vários.
Erros e Null Safety
Try / Catch
// try é expressão:
val n = try {
"42".toInt()
} catch (e: NumberFormatException) {
0
} finally {
println("tentativa feita")
}
// Múltiplos catch:
try {
operacao()
} catch (e: IllegalArgumentException) {
println("argumento: ${e.message}")
} catch (e: Exception) {
println("outro: $e")
}try é uma expressão que retorna valor — o último statement do bloco try ou catch é o resultado. O finally executa sempre. Pode ter múltiplos catch por tipo de exceção.
Safe calls encadeados
// Encadeamento seguro:
val cidade = pessoa?.endereco?.cidade?.nome
// Equivalente sem safe call (verboso):
// if (pessoa != null && pessoa.endereco != null ...)
// Com let (só executa se não-null):
pessoa?.endereco?.let { end ->
println(end.cidade)
enviarCorreio(end)
}
// Combina com elvis:
val nome = pessoa?.nome ?: "Desconhecido"Safe calls (?.) encadeiam-se — se qualquer elemento for null, o resultado é null sem NullPointerException. Combinados com let executam blocos só quando toda a cadeia existe. Substitui null-checks aninhados.
Operador !! (not-null)
var nome: String? = "Kotlin"
// Força non-null (perigoso):
val len = nome!!.length
// Se for null -> NullPointerException
// nome = null
// nome!!.length // CRASH!
// Prefira sempre alternativas seguras:
val a = nome?.length ?: 0 // elvis
val b = nome?.let { it.length } // letO operador !! converte um tipo nullable em non-null, lançando NullPointerException se for null. É um "eu sei o que faço" — evite-o. Prefira ?., ?: ou let para código verdadeiramente null-safe.
Throw e exceções
fun dividir(a: Int, b: Int): Int {
if (b == 0) {
throw IllegalArgumentException("divisão por zero")
}
return a / b
}
// throw é expressão (tipo Nothing):
fun validar(x: Int): Int =
if (x < 0) throw IllegalArgumentException("negativo")
else xthrow lança exceções (todas são Throwable). Em Kotlin não há checked exceptions — não é obrigatório declarar ou capturar. throw é uma expressão do tipo Nothing, útil em expressões if/elvis.
require e check
// require: valida argumentos (IllegalArgumentException)
fun criarUser(nome: String, idade: Int) {
require(nome.isNotBlank()) { "nome vazio" }
require(idade in 0..150) { "idade inválida: $idade" }
}
// check: valida estado (IllegalStateException)
fun iniciar() {
check(!iniciado) { "já iniciado" }
iniciado = true
}
// assert (só em debug):
assert(x > 0) { "x deve ser positivo" }require() valida argumentos de funções (lança IllegalArgumentException). check() valida o estado do objeto (lança IllegalStateException). Ambos aceitam uma mensagem lazy. Tornam as precondições explícitas e documentadas.
Plataform types
// Código Java pode retornar null: // String s = javaLib.getText(); // pode ser null // Kotlin trata como platform type (T!): val s = javaLib.getText() // String! // Compila, mas pode dar NPE em runtime // Torne explícito: val seguro: String? = javaLib.getText() val len = seguro?.length ?: 0 // @Nullable/@NotNull no Java ajudam
Platform types (T!) vêm de código Java sem anotações de nullability — o Kotlin não sabe se podem ser null. A responsabilidade é do programador. Anote como String? explicitamente e use safe calls ao interagir com Java.
Custom exceptions
class SaldoInsuficienteException(
val saldo: Double,
val valor: Double
) : Exception("Saldo $saldo insuficiente para $valor")
fun levantar(saldo: Double, valor: Double) {
if (valor > saldo) {
throw SaldoInsuficienteException(saldo, valor)
}
}
try {
levantar(100.0, 150.0)
} catch (e: SaldoInsuficienteException) {
println(e.saldo)
}Crie exceções customizadas estendendo Exception (recuperável) ou RuntimeException. Adicione propriedades relevantes (como saldo) para contexto. Mantenha a hierarquia de exceções simples e específica.
Result type
val resultado: Result<Int> = runCatching {
"42".toInt()
}
resultado.onSuccess { println("Ok: $it") }
resultado.onFailure { println("Erro: $it") }
// Obter valor:
val n = resultado.getOrDefault(0)
val n2 = resultado.getOrNull()
val n3 = resultado.getOrThrow()
// Encadear:
val dobro = resultado.map { it * 2 }Result<T> encapsula sucesso ou falha sem lançar exceção. runCatching{} captura exceções num Result. onSuccess/onFailure tratam cada caso; map transforma o sucesso. Alternativa funcional a try/catch.
Elvis operator
// Default se null:
val len = nome?.length ?: 0
// Com throw:
fun obterNome(): String {
return input?.trim()
?: throw IllegalStateException("sem nome")
}
// Encadeado:
val valor = a ?: b ?: c ?: "default"
// Com return:
fun processar(x: Int?) {
val n = x ?: return // early return se null
println(n * 2)
}O operador elvis (?:) fornece um valor alternativo quando o lado esquerdo é null. Combina com throw (lança se null), return (early exit) e encadeamento. É a ferramenta central da null-safety idiomática.
runCatching
// Em vez de try/catch:
val valor = runCatching {
api.fetchDados()
}.getOrElse { erro ->
println("Falha: $erro")
dadosLocais()
}
// Transformar resultado:
val processado = runCatching { parse(input) }
.map { it.uppercase() }
.recover { "default" }
.getOrThrow()runCatching{} executa um bloco e captura qualquer exceção num Result. getOrElse fornece alternativa em caso de falha; recover transforma o erro em valor. Permite pipelines funcionais sem try/catch verboso.
Ferramentas e Idioms
Idioms essenciais
// Singleton:
object Recurso { }
// Imutável por defeito:
val lista = listOf(1, 2, 3)
// Expressões em tudo:
val max = if (a > b) a else b
// Data class para modelos:
data class User(val nome: String)
// Scope functions:
val s = StringBuilder().apply { append("oi") }Idioms Kotlin: prefira val (imutável), use data class para modelos, expressões em vez de statements, object para singletons e scope functions para eliminar variáveis temporárias. Código conciso e expressivo.
Testes
import kotlin.test.*
class CalculadoraTest {
@Test
fun `soma dois números`() {
val r = soma(2, 3)
assertEquals(5, r)
}
@Test
fun `lança em divisão por zero`() {
assertFailsWith<IllegalArgumentException> {
dividir(1, 0)
}
}
@BeforeTest fun setup() { }
@AfterTest fun cleanup() { }
}Kotlin Test (kotlin.test) fornece asserções multiplataforma. Testes podem ter nomes com espaços entre crases. assertEquals verifica igualdade, assertFailsWith verifica exceções. @BeforeTest/@AfterTest fazem setup/cleanup.
Performance e boxing
// Primitivos otimizados:
val arr = IntArray(1000) { it } // sem boxing
val lista = List(1000) { it } // Int? boxed
// inline evita alocação de lambdas:
inline fun repetir(n: Int, bloco: () -> Unit) {
for (i in 0 until n) bloco()
}
// Sequences para grandes coleções:
val r = dados.asSequence()
.filter { it > 0 }
.map { it * 2 }
.take(10)
.toList()Use IntArray/DoubleArray para evitar boxing. inline elimina alocação de objetos lambda em loops. Sequences evitam listas intermédias em cadeias de operações. Prefira val e imutabilidade para otimizações do compilador.
Stdlib útil
// Strings:
"olá".uppercase() // "OLÁ"
"a,b,c".split(",") // [a, b, c]
" x ".trim() // "x"
// Números:
val r = (1..10).random()
val n = 3.14159.roundToInt() // 3
// Coleções:
listOf(1, 2, 3).shuffled()
listOf(1, 2, 3).reversed()
"a" to 1 // PairA stdlib do Kotlin é rica: uppercase/split/trim para strings, random/roundToInt para números, shuffled/reversed para coleções. O to cria Pair. Reduz muito o código utilitário.
Coroutines test
import kotlinx.coroutines.test.*
@Test
fun testeAsync() = runTest {
// delay é saltado automaticamente:
val resultado = fetchDados() // suspend
assertEquals("dados", resultado)
}
@Test
fun testeConcorrente() = runTest {
val r1 = async { operacao1() }
val r2 = async { operacao2() }
assertEquals(42, r1.await() + r2.await())
}kotlinx-coroutines-test fornece runTest que executa coroutines de teste de forma determinística — delay é saltado (virtual time), tornando testes rápidos. async/launch funcionam normalmente dentro do teste.
Boas práticas
// 1. val por defeito, var só se necessário val total = calcular() // 2. Imutável: listOf em vez de mutableListOf val itens = listOf(1, 2, 3) // 3. Expressões em vez de statements val r = if (x > 0) x else -x // 4. data class para modelos data class Dto(val id: Int, val nome: String) // 5. Null-safety em vez de !! val n = valor?.length ?: 0
Boas práticas: val por defeito, coleções imutáveis, expressões em vez de statements, data class para modelos e null-safety idiomática (nunca !!). Siga o Kotlin Coding Conventions oficial para estilo consistente.
Interop com Java
// Kotlin chama Java diretamente:
val lista = java.util.ArrayList<String>()
val data = java.time.LocalDate.now()
// Java chama Kotlin:
// (compila para bytecode JVM normal)
// Anotações para interop:
@JvmStatic fun util() {} // static real
@JvmOverloads fun f(a: Int, b: Int = 0) {}
@JvmField val x = 1 // campo público
@file:JvmName("Utils") // nome do ficheiroKotlin é 100% interoperável com Java — chama bibliotecas Java diretamente e vice-versa. Anotações @JvmStatic, @JvmOverloads, @JvmField e @JvmName controlam como o código Kotlin aparece para o Java.
Multiplatform
// commonMain (partilhado):
expect fun plataforma(): String
// androidMain:
actual fun plataforma() = "Android"
// iosMain:
actual fun plataforma() = "iOS"
// jvmMain:
actual fun plataforma() = "JVM"
// Uso no código comum:
fun saudar() = "Olá de ${plataforma()}"Kotlin Multiplatform partilha lógica de negócio entre plataformas (Android, iOS, JVM, JS, Native). expect declara uma API no código comum; actual fornece a implementação específica de cada plataforma. Ideal para networking, modelos e validação.
Gradle Kotlin DSL
// build.gradle.kts:
plugins {
kotlin("jvm") version "1.9.0"
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
testImplementation(kotlin("test"))
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(17)
}O Gradle Kotlin DSL (build.gradle.kts) usa Kotlin em vez de Groovy — type-safe, autocompletar no IDE e refatorável. plugins{} aplica plugins, dependencies{} gere dependências. É o padrão recomendado para projetos novos.
Null-safe design
// Retorne não-null sempre que possível:
fun nome(): String = "Ana" // nunca null
// Use nullable só quando necessário:
fun encontrar(id: Int): User? =
base.firstOrNull { it.id == id }
// Force o tratamento no call site:
val u = encontrar(1)
?: throw NotFoundException("user 1")
// Tipos não-null + elvis = código seguroDesign null-safe: retorne tipos não-null por defeito; use nullable (?) só quando a ausência é um estado válido. Force o tratamento no call site com ?: ou let. Quanto menos nullables na API, mais seguro o código.