Neste tutorial, vamos explorar o sistema de collections do Kotlin de forma completa e prática. Collections (coleções) são estruturas de dados fundamentais em qualquer linguagem de programação, e o Kotlin oferece uma API rica e expressiva para trabalhar com listas, conjuntos e mapas. Você aprenderá a diferença entre coleções mutáveis e imutáveis, as operações mais comuns como filter, map, flatMap e groupBy, além de sequences para processamento eficiente e collection builders para construção flexível.
List: Coleções Ordenadas
List é uma coleção ordenada que permite elementos duplicados. Em Kotlin, listOf() cria uma lista imutável (somente leitura), enquanto mutableListOf() cria uma lista que pode ser modificada.
fun main() {
// Lista imutável
val linguagens = listOf("Kotlin", "Java", "Python", "Kotlin") // duplicatas OK
println(linguagens) // [Kotlin, Java, Python, Kotlin]
println(linguagens[0]) // Kotlin
println(linguagens.size) // 4
println(linguagens.contains("Java")) // true
println("Python" in linguagens) // true (operador 'in')
// Lista mutável
val tarefas = mutableListOf("Estudar", "Codar")
tarefas.add("Testar")
tarefas.add(1, "Planejar") // Insere na posição 1
tarefas.removeAt(0) // Remove "Estudar"
tarefas[0] = "Arquitetar" // Substitui "Planejar"
println(tarefas) // [Arquitetar, Codar, Testar]
// Conversão entre mutável e imutável
val imutavel: List<String> = tarefas.toList()
val mutavel: MutableList<String> = linguagens.toMutableList()
// Lista tipada vazia
val vazia = emptyList<Int>()
val vaziaComTipo: List<String> = listOf()
println(vazia.isEmpty()) // true
}
A distinção entre List (somente leitura) e MutableList é uma escolha de design do Kotlin que promove imutabilidade. Quando você passa uma List para uma função, quem recebe tem a garantia de que a lista não será modificada por aquela referência, tornando o código mais previsível.
Set: Coleções sem Duplicatas
Set é uma coleção que não permite elementos duplicados. setOf() cria um set imutável e mutableSetOf() cria um set mutável. A implementação padrão mantém a ordem de inserção (LinkedHashSet).
fun main() {
val frutas = setOf("Maçã", "Banana", "Maçã", "Laranja")
println(frutas) // [Maçã, Banana, Laranja] — sem duplicata
println(frutas.size) // 3
// Operações de conjunto
val frutasA = setOf("Maçã", "Banana", "Laranja")
val frutasB = setOf("Banana", "Uva", "Morango")
println(frutasA union frutasB) // [Maçã, Banana, Laranja, Uva, Morango]
println(frutasA intersect frutasB) // [Banana]
println(frutasA subtract frutasB) // [Maçã, Laranja]
// Set mutável
val tags = mutableSetOf("kotlin", "android")
tags.add("kotlin") // Não adiciona (já existe)
tags.add("jetpack")
println(tags) // [kotlin, android, jetpack]
// Remover duplicatas de uma lista
val comDuplicatas = listOf(1, 2, 3, 2, 1, 4, 3, 5)
val semDuplicatas = comDuplicatas.toSet().toList()
println(semDuplicatas) // [1, 2, 3, 4, 5]
// distinct() é um atalho
println(comDuplicatas.distinct()) // [1, 2, 3, 4, 5]
}
Sets são ideais para verificações de pertencimento (operação in) e para garantir unicidade. A busca em um HashSet tem complexidade O(1), muito mais eficiente do que buscar em uma lista.
Map: Coleções de Chave-Valor
Map armazena pares de chave-valor, onde cada chave é única. mapOf() cria um map imutável e mutableMapOf() cria um map mutável.
fun main() {
// Map imutável
val capitais = mapOf(
"Brasil" to "Brasília",
"Argentina" to "Buenos Aires",
"Chile" to "Santiago"
)
println(capitais["Brasil"]) // Brasília
println(capitais["Portugal"]) // null (chave não existe)
println(capitais.getOrDefault("Portugal", "Desconhecida")) // Desconhecida
// Iteração
for ((pais, capital) in capitais) {
println("$pais → $capital")
}
// Map mutável
val estoque = mutableMapOf(
"Camiseta" to 50,
"Calça" to 30
)
estoque["Tênis"] = 20 // Adiciona novo par
estoque["Camiseta"] = 45 // Atualiza valor existente
estoque.remove("Calça") // Remove par
println(estoque) // {Camiseta=45, Tênis=20}
// getOrPut: retorna o valor existente ou insere e retorna o novo
val cache = mutableMapOf<String, Int>()
val valor = cache.getOrPut("chave") {
println("Calculando...")
42
}
println(valor) // 42 (calculou e inseriu)
println(cache.getOrPut("chave") { 100 }) // 42 (já existia, não recalcula)
// Verificações
println("Camiseta" in estoque) // true (verifica chave)
println(estoque.containsValue(20)) // true (verifica valor)
}
O operador to é uma função infix que cria um Pair. A expressão "Brasil" to "Brasília" é equivalente a Pair("Brasil", "Brasília"). Maps são essenciais para caches, configurações e qualquer cenário de busca por chave.
Operações Comuns: filter, map e flatMap
O Kotlin oferece uma vasta coleção de funções de transformação inspiradas na programação funcional. Essas operações usam lambdas e retornam novas coleções, sem modificar a original.
data class Produto(val nome: String, val preco: Double, val categoria: String)
fun main() {
val produtos = listOf(
Produto("Notebook", 4500.0, "Eletrônicos"),
Produto("Teclado", 250.0, "Periféricos"),
Produto("Mouse", 120.0, "Periféricos"),
Produto("Monitor", 2200.0, "Eletrônicos"),
Produto("Cadeira", 1800.0, "Móveis"),
Produto("Mesa", 900.0, "Móveis")
)
// filter: seleciona elementos que atendem a condição
val caros = produtos.filter { it.preco > 1000 }
println("Caros: ${caros.map { it.nome }}")
// Caros: [Notebook, Monitor, Cadeira]
// map: transforma cada elemento
val nomes = produtos.map { it.nome.uppercase() }
println("Nomes: $nomes")
// Nomes: [NOTEBOOK, TECLADO, MOUSE, MONITOR, CADEIRA, MESA]
// map com transformação complexa
val resumos = produtos.map { "${it.nome}: R$${"%.2f".format(it.preco)}" }
println(resumos)
// flatMap: transforma e achata listas aninhadas
val categorias = listOf(
listOf("Kotlin", "Java"),
listOf("Python", "Ruby"),
listOf("Go", "Rust")
)
val todasLinguagens = categorias.flatMap { it }
println(todasLinguagens) // [Kotlin, Java, Python, Ruby, Go, Rust]
// flatMap prático: caracteres de cada nome
val letras = produtos.flatMap { it.nome.toList() }.distinct()
println("Letras únicas: ${letras.size}")
// Encadeamento de operações
val resultado = produtos
.filter { it.preco < 2000 }
.sortedByDescending { it.preco }
.map { "${it.nome} (${it.categoria})" }
println("Resultado: $resultado")
// Resultado: [Cadeira (Móveis), Mesa (Móveis), Teclado (Periféricos), Mouse (Periféricos)]
}
O encadeamento de operações é uma das grandes forças do Kotlin. Cada operação retorna uma nova coleção, permitindo criar pipelines de transformação expressivos e legíveis.
Operações Avançadas: groupBy e associate
groupBy e associate são operações poderosas para reorganizar dados em mapas.
data class Aluno(val nome: String, val turma: String, val nota: Double)
fun main() {
val alunos = listOf(
Aluno("Ana", "A", 8.5),
Aluno("Bruno", "B", 7.0),
Aluno("Carla", "A", 9.2),
Aluno("Diego", "B", 6.5),
Aluno("Eva", "A", 7.8),
Aluno("Felipe", "B", 8.0)
)
// groupBy: agrupa elementos por uma chave
val porTurma: Map<String, List<Aluno>> = alunos.groupBy { it.turma }
porTurma.forEach { (turma, lista) ->
println("Turma $turma: ${lista.map { it.nome }}")
}
// Turma A: [Ana, Carla, Eva]
// Turma B: [Bruno, Diego, Felipe]
// Média por turma
val mediaPorTurma = alunos.groupBy { it.turma }
.mapValues { (_, alunosTurma) -> alunosTurma.map { it.nota }.average() }
println("Médias: $mediaPorTurma")
// Médias: {A=8.5, B=7.166666666666667}
// associate: cria map a partir de transformação
val notasPorNome: Map<String, Double> = alunos.associate { it.nome to it.nota }
println("Notas: $notasPorNome")
// Notas: {Ana=8.5, Bruno=7.0, Carla=9.2, Diego=6.5, Eva=7.8, Felipe=8.0}
// associateBy: usa uma propriedade como chave
val alunoPorNome: Map<String, Aluno> = alunos.associateBy { it.nome }
println(alunoPorNome["Carla"]) // Aluno(nome=Carla, turma=A, nota=9.2)
// partition: divide em dois grupos (par de listas)
val (aprovados, reprovados) = alunos.partition { it.nota >= 7.0 }
println("Aprovados: ${aprovados.map { it.nome }}") // [Ana, Bruno, Carla, Eva, Felipe]
println("Reprovados: ${reprovados.map { it.nome }}") // [Diego]
// Outras operações úteis
println("Melhor nota: ${alunos.maxByOrNull { it.nota }?.nome}") // Carla
println("Pior nota: ${alunos.minByOrNull { it.nota }?.nome}") // Diego
println("Soma notas: ${alunos.sumOf { it.nota }}") // 47.0
println("Todos aprovados: ${alunos.all { it.nota >= 7.0 }}") // false
println("Algum com 10: ${alunos.any { it.nota == 10.0 }}") // false
}
Sequences: Processamento Lazy
Por padrão, operações de coleção em Kotlin são eager — cada operação processa todos os elementos e cria uma coleção intermediária. Sequences processam elementos um a um (lazy), o que é mais eficiente para coleções grandes ou cadeias com muitas operações.
fun main() {
val numeros = (1..1_000_000).toList()
// EAGER: cria lista intermediária a cada passo
val resultadoEager = numeros
.filter { it % 2 == 0 } // Cria lista com 500.000 elementos
.map { it * 3 } // Cria outra lista com 500.000 elementos
.take(5) // Pega apenas 5 (desperdiçou processamento!)
println(resultadoEager) // [6, 12, 18, 24, 30]
// LAZY (Sequence): processa elemento a elemento, para quando tem 5
val resultadoLazy = numeros.asSequence()
.filter { it % 2 == 0 }
.map { it * 3 }
.take(5)
.toList() // Terminal operation: dispara o processamento
println(resultadoLazy) // [6, 12, 18, 24, 30]
// Gerando sequences diretamente
val fibonacci = generateSequence(Pair(0L, 1L)) { (a, b) -> Pair(b, a + b) }
.map { it.first }
.take(10)
.toList()
println("Fibonacci: $fibonacci")
// Fibonacci: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// sequence builder com yield
val potenciasDeDois = sequence {
var valor = 1
while (true) {
yield(valor)
valor *= 2
}
}
println(potenciasDeDois.take(10).toList())
// [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
}
Use sequences quando tiver coleções grandes (milhares de elementos) ou cadeias longas de transformações. Para coleções pequenas, a diferença é negligível e coleções normais podem ser até mais rápidas devido ao overhead das sequences.
Collection Builders
Kotlin oferece funções builder para construir coleções de forma condicional e programática, mantendo a imutabilidade no resultado final.
fun construirMenu(admin: Boolean, premium: Boolean): List<String> {
return buildList {
add("Início")
add("Perfil")
add("Configurações")
if (premium) {
add("Conteúdo Exclusivo")
add("Suporte Prioritário")
}
if (admin) {
addAll(listOf("Painel Admin", "Gerenciar Usuários", "Relatórios"))
}
add("Sair")
}
}
fun construirConfiguracoes(params: Map<String, String>): Map<String, String> {
return buildMap {
put("versao", "1.0.0")
put("ambiente", "producao")
putAll(params) // Adiciona todas as configurações recebidas
if (!containsKey("timeout")) {
put("timeout", "30")
}
}
}
fun main() {
println(construirMenu(admin = false, premium = true))
// [Início, Perfil, Configurações, Conteúdo Exclusivo, Suporte Prioritário, Sair]
println(construirMenu(admin = true, premium = false))
// [Início, Perfil, Configurações, Painel Admin, Gerenciar Usuários, Relatórios, Sair]
val config = construirConfiguracoes(mapOf("regiao" to "br", "timeout" to "60"))
println(config)
// {versao=1.0.0, ambiente=producao, regiao=br, timeout=60}
}
Collection builders como buildList, buildSet e buildMap foram introduzidos no Kotlin 1.6 como estáveis. Eles permitem construir coleções imutáveis com lógica condicional, algo que antes exigia criar uma coleção mutável e convertê-la depois.
Erros Comuns
Confundir List e MutableList. Tentar adicionar elementos a uma List retornada por listOf() causa erro. Se precisar modificar, use toMutableList() ou declare diretamente como mutableListOf().
Modificar coleção durante iteração. Iterar sobre uma MutableList e remover elementos simultaneamente causa ConcurrentModificationException. Use removeIf, filter ou itere sobre uma cópia.
val lista = mutableListOf(1, 2, 3, 4, 5)
// ERRADO: for (item in lista) { if (item > 3) lista.remove(item) }
lista.removeIf { it > 3 } // CORRETO
Usar sequences para coleções pequenas. O overhead de sequences não compensa para listas com poucos elementos. Reserve-as para cenários com muitos dados ou cadeias longas de operações.
Ignorar a diferença entre map e flatMap. Usar map quando a lambda retorna uma lista produz List<List<T>>. Se você quer achatar o resultado, use flatMap.
Conclusão e Próximos Passos
Neste tutorial, você aprendeu a usar collections em Kotlin de forma completa: List, Set e Map com suas variantes mutáveis e imutáveis, operações de transformação como filter, map, flatMap, groupBy e associate, sequences para processamento lazy e collection builders para construção condicional.
O domínio de collections é fundamental para qualquer desenvolvedor Kotlin, pois praticamente toda aplicação manipula coleções de dados. O próximo passo é explorar higher-order functions e lambdas em maior profundidade, além de coroutines para processamento assíncrono de coleções com Flow. Recomendamos também estudar destructuring para extrair dados de mapas e data classes de forma elegante. Com esse conhecimento de collections, você está preparado para escrever código Kotlin funcional, conciso e performático.