Collections em Kotlin são estruturas para armazenar e processar grupos de valores. Use List quando a ordem e as repetições importam, Set quando os elementos devem ser únicos e Map para associar chaves a valores. As interfaces List, Set e Map oferecem acesso somente leitura; para adicionar, remover ou atualizar elementos, use MutableList, MutableSet ou MutableMap.
| Necessidade | Tipo indicado | Criação comum |
|---|---|---|
| Manter ordem e aceitar duplicatas | List<T> | listOf(...) |
| Alterar uma lista | MutableList<T> | mutableListOf(...) |
| Garantir valores únicos | Set<T> | setOf(...) |
| Buscar um valor por uma chave | Map<K, V> | mapOf(chave to valor) |
| Processar uma cadeia longa sob demanda | Sequence<T> | lista.asSequence() |
Neste tutorial, vamos explorar o sistema de collections do Kotlin de forma completa e prática. Coleções são estruturas de dados fundamentais, e o Kotlin oferece uma API rica e expressiva para trabalhar com listas, conjuntos e mapas. Você aprenderá a diferença entre interfaces somente leitura e mutá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.
Importante: “somente leitura” não significa necessariamente “imutável”. Uma
List<T>não expõe operações de alteração, mas pode ser uma visão de uma lista mutável que outra referência ainda consegue modificar. Se você precisa de um snapshot independente, crie uma cópia comtoList().
List: Coleções Ordenadas
List é uma coleção ordenada que permite elementos duplicados. Em Kotlin, listOf() cria uma lista somente leitura, enquanto mutableListOf() cria uma lista que pode ser modificada.
fun main() {
// Lista somente leitura
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]
// Cópias entre representações somente leitura e mutável
val somenteLeitura: 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 ajuda a limitar quem pode alterar uma coleção. Quando uma função recebe List<T>, ela não pode chamar add, remove ou substituir itens por essa referência. Isso torna o contrato mais previsível, embora não impeça que outra referência mutável altere o mesmo objeto.
Set: Coleções sem Duplicatas
Set é uma coleção que não permite elementos duplicados. setOf() cria um set somente leitura 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() e 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, em que cada chave é única. mapOf() cria um map somente leitura e mutableMapOf() cria um map mutável.
fun main() {
// Map somente leitura
val capitais = mapOf(
"Brasil" to "Brasília",
"Argentina" to "Buenos Aires",
"Chile" to "Santiago"
)
println(capitais["Brasil"]) // Brasília
println(capitais["Portugal"]) // null (chave nao 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, nao 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 condicao
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 pratico: caracteres de cada nome
val letras = produtos.flatMap { it.nome.toList() }.distinct()
println("Letras unicas: ${letras.size}")
// Encadeamento de operacoes
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 operacoes uteis
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, retornando interfaces somente leitura 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("região" to "br", "timeout" to "60"))
println(config)
// {versao=1.0.0, ambiente=producao, região=br, timeout=60}
}
Collection builders como buildList, buildSet e buildMap foram introduzidos no Kotlin 1.6 como estáveis. Eles permitem construir coleções expostas como somente leitura com lógica condicional, algo que antes normalmente exigia criar uma coleção mutável e convertê-la depois.
Erros Comuns
Confundir List e MutableList. Uma referência do tipo List não oferece add ou remove; a tentativa nem compila. Se precisar modificar os dados, use toMutableList() para criar uma cópia ou declare a coleção como mutableListOf() desde o início.
Tratar somente leitura como imutabilidade profunda. Copiar a coleção com toList() separa sua estrutura, mas não copia os objetos mutáveis armazenados nela. Se os elementos também precisam ser independentes, faça uma cópia de cada elemento.
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.
Perguntas Frequentes
Qual é a diferença entre List e MutableList em Kotlin?
List permite consultar e percorrer elementos, mas não expõe operações de alteração. MutableList acrescenta métodos como add, remove e set. Prefira receber List em APIs que apenas leem dados e use MutableList somente onde a mutação é necessária.
Quando usar Set em vez de List?
Use Set quando valores duplicados não fazem sentido ou quando verificações de pertencimento são frequentes. Use List quando você precisa preservar repetições, acessar elementos por índice ou representar uma sequência ordenada.
map modifica a collection original?
Não. A função map transforma cada elemento e devolve uma nova lista. Para atualizar uma coleção mutável existente, você precisa alterar seus itens explicitamente; ainda assim, pipelines que retornam novas coleções costumam ser mais fáceis de entender.
Quando Sequence é melhor que List?
Sequence tende a ajudar em cadeias longas de operações, grandes volumes ou processamento que pode parar cedo, como filter().map().take(5). Para coleções pequenas ou uma única operação, uma List normalmente é mais simples e pode ser mais rápida.
Conclusão e Próximos Passos
Neste tutorial, você aprendeu a usar collections em Kotlin de forma completa: List, Set e Map com suas interfaces somente leitura e mutá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.