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.

NecessidadeTipo indicadoCriação comum
Manter ordem e aceitar duplicatasList<T>listOf(...)
Alterar uma listaMutableList<T>mutableListOf(...)
Garantir valores únicosSet<T>setOf(...)
Buscar um valor por uma chaveMap<K, V>mapOf(chave to valor)
Processar uma cadeia longa sob demandaSequence<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 com toList().

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.