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.