Pular para o conteúdo

Trilha de Kotlin · parte 6 de 59

Coleções e lambdas

Listas, conjuntos e mapas no Kotlin, lambdas com it e as operações do dia a dia, como map, filter e groupBy, com saídas reais do Kotlin 2.4.21.

iniciantePublicada em · 13 min de leitura

Tamanho da fonte:
Largura da coluna:
Tom:
Espaço entre linhas:
Fonte:

Coleções e lambdas

Uma é a sacola de feira do programa. Ela guarda muitos itens sob um nome só, e você percorre, tira ou troca cada um quando precisa. A é o bilhete com a instrução, como “some o frete a este pedido”, que você entrega a quem percorre a sacola.

o mapa da aula

A aula vai das coleções às lambdas, passa pelas operações mais usadas e termina com exercícios. Os trechos foram compilados e executados com Kotlin 2.4.21 pelo Gradle 9.7.0, sobre o OpenJDK 25, como na aula de Ambiente e primeiro programa. As saídas foram copiadas do programa. Os dois erros de compilação saíram de um projeto separado, com o caminho do arquivo abreviado. Não testei no IntelliJ. Sequence, imutabilidade profunda e custo das lambdas ficam para a fase 3, em Coleções imutáveis e Sequence e Funções inline e custo de lambdas.

1. para que servem as coleções

Um pedido com cinco itens em variáveis separadas quebra no dia em que chega o sexto. Uma coleção guarda os itens sob um nome, diz quantos são e permite falar do conjunto inteiro: somar os valores de um pedido serve para cinco itens ou para quinhentos.

2. , e

List guarda itens em ordem e aceita repetidos, como as linhas de um pedido. Set guarda itens sem repetição, útil para perguntar se um valor já está presente. A documentação diz que um Set não garante a ordem de iteração (“doesn’t guarantee iteration order”, em tradução livre), então não conte com ela. Map guarda pares de chave e valor, com cada chave única, como um produto e o seu preço.

package basico

fun main() {
    val frutas = listOf("maçã", "banana", "maçã")
    val tags = setOf("kotlin", "jvm", "kotlin")
    val precos = mapOf("maçã" to 3, "banana" to 2)

    println("frutas: ${frutas.size} itens")
    println("tags: ${tags.size} itens")
    println("precos: ${precos.size} chaves")
    println(frutas[1])
    println(precos["banana"])
    for (fruta in frutas) println("fruta: $fruta")
    for ((nome, preco) in precos) println("$nome custa $preco")
}
frutas: 3 itens
tags: 2 itens
precos: 2 chaves
banana
2
fruta: maçã
fruta: banana
fruta: maçã
maçã custa 3
banana custa 2

A lista tem três itens porque aceita o repetido, e o set tem dois porque descarta o segundo “kotlin”. A expressão "maçã" to 3 cria um , e é assim que o mapOf recebe as entradas. O colchete em precos["banana"] busca pela chave, e o (nome, preco) do laço desmonta cada par.

3. somente leitura e mutável

Cada tipo tem uma versão somente leitura (List, Set, Map), que só dá acesso, e uma mutável (MutableList e assim por diante), que acrescenta adicionar, remover e trocar. listOf, setOf e mapOf criam as primeiras, e as funções com mutable no nome criam as segundas. Se uma função recebe uma List, o compilador impede que ela a altere. Chamar add numa List dá este erro. O arquivo ListaSomenteLeitura.kt não está nesta nota:

e: .../ListaSomenteLeitura.kt:5:11 Unresolved reference 'add' on receiver of type 'List<Int>'.

O nome “somente leitura” promete mais do que a interface entrega. Ela limita o que você faz pela referência usada, mas não protege o objeto:

package leitura

fun main() {
    val editavel = mutableListOf("a")
    val leitura: List<String> = editavel
    editavel.add("b")
    println(leitura)
    println(leitura.size)
}
[a, b]
2

As duas variáveis apontam para a mesma lista. Assim, leitura, que não aceita add, enxerga a mudança feita por editavel. A figura mostra o caminho de cada chamada.

Duas referências, uma MutableList e uma List, apontam para a mesma lista; add pelo nome mutável aparece na leitura, e add pela List não compila
figura 1 · Duas referências apontando para a mesma lista

O pacote azul é o add feito pela referência mutável, e o verde é a leitura do mesmo objeto. A chamada vermelha é recusada pelo compilador e nem chega a rodar. Nas assinaturas, use List para quem só lê. A imutabilidade de verdade, em que o próprio objeto não muda, fica para a fase 3.

4. acessar, adicionar e percorrer

lista[0] pega o primeiro item, lista.size diz quantos são, e mapa["banana"] busca pela chave. Quando a chave não existe, o resultado é null, assunto da seção 8. Para incluir, use add numa lista mutável, ou mapa["chave"] = valor num mapa mutável. Rodei as duas formas, em trechos que não estão nesta nota: um add(2) sobre mutableListOf(1) imprime [1, 2], e um mapa mutável que recebe b e c e perde a com remove imprime {b=2, c=3}. Para percorrer, use o for da aula de controle de fluxo ou as operações da seção 6.

Pergunta de entrevistaQual a diferença entre List e MutableList em Kotlin, e uma List é realmente imutável?

List, Set e Map são as versões somente leitura, que só dão acesso aos elementos. MutableList e as demais acrescentam adicionar, remover e trocar. Se uma função recebe uma List, o compilador impede que ela chame add. Somente leitura limita o que posso fazer pela referência usada e não protege o objeto. Na aula, uma MutableList e uma List apontam para a mesma lista, e o add feito pela referência mutável aparece na leitura. Por isso uso List nas assinaturas de quem só lê e mantenho a MutableList local para quem constrói a coleção. Se preciso de imutabilidade de verdade, em que o próprio objeto não muda, o tipo List sozinho não basta.

5. lambdas

Uma lambda é uma função sem nome, escrita onde é usada e passada como valor. A documentação a chama de literal de função: uma função que não é declarada e é passada imediatamente como expressão (tradução livre). A sintaxe tem três partes: chaves, parâmetros antes de uma seta e corpo depois dela. Em val dobro = { x: Int -> x * 2 }, a variável guarda uma função do tipo (Int) -> Int. Com um só parâmetro, o nome automático é it, e { it * 10 } equivale a { x -> x * 10 }. Se a lambda é o último argumento, ela pode ficar depois dos parênteses, e isso se chama trailing lambda.

package lambdas

fun aplicar(valor: Int, operacao: (Int) -> Int): Int = operacao(valor)

fun main() {
    val dobro = { x: Int -> x * 2 }
    println(dobro(5))

    val numeros = listOf(1, 2, 3)
    println(numeros.map { it * 10 })
    println(numeros.map { x -> x * 10 })

    println(aplicar(5) { it + 1 })
    println(aplicar(5, dobro))
}
10
[10, 20, 30]
[10, 20, 30]
6
10

A função aplicar recebe um número e uma função. A aula de Funções explica como declarar esse tipo de parâmetro. A lambda guardada em dobro entra como argumento do mesmo jeito que uma escrita no lugar.

6. operações do dia a dia

Os exemplos usam uma lista de pedidos, cada um um par com cliente e valor.

package operacoes

fun main() {
    val pedidos = listOf("Ana" to 120, "Bruno" to 45, "Ana" to 80, "Caio" to 200, "Bruno" to 30)

    pedidos.forEach { (cliente, valor) -> println("$cliente: $valor") }
    println(pedidos.map { (cliente, _) -> cliente })
    println(pedidos.filter { (_, valor) -> valor >= 50 })
    println(pedidos.first())
    println(pedidos.firstOrNull { (_, valor) -> valor > 500 })
    println(pedidos.find { (cliente, _) -> cliente == "Caio" })
    println(pedidos.any { (_, valor) -> valor > 150 })
    println(pedidos.all { (_, valor) -> valor > 0 })
    println(pedidos.sortedBy { (_, valor) -> valor })
    println(pedidos.groupBy { (cliente, _) -> cliente })

    val valores = pedidos.map { (_, valor) -> valor }
    println(valores.sum())
    println(valores.count { it >= 50 })
    println(valores.average())
    println(valores.joinToString(" | "))
}
Ana: 120
Bruno: 45
Ana: 80
Caio: 200
Bruno: 30
[Ana, Bruno, Ana, Caio, Bruno]
[(Ana, 120), (Ana, 80), (Caio, 200)]
(Ana, 120)
null
(Caio, 200)
true
true
[(Bruno, 30), (Bruno, 45), (Ana, 80), (Ana, 120), (Caio, 200)]
{Ana=[(Ana, 120), (Ana, 80)], Bruno=[(Bruno, 45), (Bruno, 30)], Caio=[(Caio, 200)]}
475
3
95.0
120 | 45 | 80 | 200 | 30

O forEach executa um bloco para cada item e não devolve coleção, então serve para imprimir ou gravar. O map troca cada item pelo resultado da lambda e mantém o tamanho. O filter mantém os itens em que a lambda devolve true, e uma lambda que devolve Boolean é chamada de . A lista original não muda: o groupBy da saída ainda enxerga os cinco pedidos.

O first devolve o primeiro item, ou lança exceção se a lista está vazia. O firstOrNull devolve null nesse caso, e find é outro nome para ele, segundo a documentação. O any responde se algum item satisfaz a lambda, e o all responde se todos satisfazem.

O sortedBy devolve uma nova lista ordenada pelo valor que a lambda produz. O groupBy devolve um Map em que cada chave é o resultado da lambda e o valor é a lista dos itens com essa chave. Esse Map é o da seção 2, e não a função map.

Por fim, sum, count e average fecham a conta dos números. Para uma coleção de Int, average devolve Double, o que explica o 95.0. O joinToString monta uma só String com o separador escolhido.

7. encadear operações e um exercício guiado

Cada operação devolve uma coleção, e a seguinte recebe essa coleção. Essa cadeia se chama , uma linha de montagem. A figura mostra a lista de pedidos passando por filter, map e sum.

Cinco pedidos entram no filter, que deixa passar os de 50 ou mais; o map troca cada par pelo valor e o sum soma 120, 80 e 200 até 400
figura 2 · Filter, map e sum: pedidos que viram um total

Os pedidos de 120, 80 e 200 seguem pelo caminho do sim, em linha grossa, até o sum. Os de 45 e 30 seguem pelo caminho pontilhado do não, para a caixa rosa, e não chegam à soma. Verde são os dados, azul as operações, laranja o resultado e rosa o que ficou de fora.

No "Ana" to 120, o par tem duas partes, acessadas por first e second. Dentro da lambda, (cliente, valor) dá nomes a elas, e o _ marca a parte que não interessa.

O exercício guiado pede um relatório com os pedidos grandes, o total e a quantidade de pedidos por cliente. Filtre, transforme, some e agrupe, rodando o programa a cada passo.

package guiado

fun main() {
    val pedidos = listOf("Ana" to 120, "Bruno" to 45, "Ana" to 80, "Caio" to 200, "Bruno" to 30)

    val grandes = pedidos.filter { (_, valor) -> valor >= 50 }
    println("grandes: ${grandes.size}")

    val frases = grandes.map { (cliente, valor) -> "$cliente pediu $valor reais" }
    frases.forEach { println(it) }

    val total = grandes.map { (_, valor) -> valor }.sum()
    println("total: $total")

    val porCliente = grandes.groupBy { (cliente, _) -> cliente }
    for ((cliente, lista) in porCliente) println("$cliente: ${lista.size} pedido(s)")

    val encadeado = pedidos.filter { it.second >= 50 }.map { it.second }.sum()
    println("encadeado: $encadeado")
}
grandes: 3
Ana pediu 120 reais
Ana pediu 80 reais
Caio pediu 200 reais
total: 400
Ana: 2 pedido(s)
Caio: 1 pedido(s)
encadeado: 400

O Bruno não aparece no agrupamento porque os pedidos dele ficaram de fora do filtro. Para contar todos os clientes, agrupe a lista original, antes do filtro.

8. erros comuns e boas práticas

modificar uma lista somente leitura

O add numa List é pego pelo compilador, como vimos na seção 3. O perigo é uma referência mutável guardada em outro lugar, porque ela muda a lista sem aviso. Escreva List nas assinaturas e não entregue mutáveis a quem só deveria ler.

first em lista vazia

O código abaixo roda três operações sobre uma lista sem itens:

package vazio

fun main() {
    val semPedidos = emptyList<Int>()
    println(semPedidos.firstOrNull())
    println(semPedidos.average())
    println(semPedidos.first())
}
null
NaN
Exception in thread "main" java.util.NoSuchElementException: List is empty.
	at kotlin.collections.CollectionsKt___CollectionsKt.first(_Collections.kt:222)
	at vazio.VazioKt.main(Vazio.kt:7)

A última linha da pilha foi omitida. O first() supõe que existe um primeiro item. Quando não existe, lança NoSuchElementException, e por isso é otimista demais. O firstOrNull() devolve null e obriga você a tratar o caso. Se lista vazia é normal, use firstOrNull. O average() da mesma lista devolveu NaN sem lançar exceção. Esse resultado foi observado no programa, porque a página da API consultada não cita o caso.

Map.get devolve nulo

Sem a chave, precos["abacaxi"] devolve null, e o tipo Int? obriga você a decidir o que fazer. Guardar esse valor num Int dá este erro, num arquivo MapaSemNulo.kt que não está nesta nota:

e: .../MapaSemNulo.kt:5:20 Initializer type mismatch: expected 'Int', actual 'Int?'.

O operador elvis, ?:, usa o valor da direita quando o da esquerda é null. Por isso precos["abacaxi"] ?: 0 imprime 0. A aula Null safety e tipos nuláveis detalha esse operador.

map de função e Map de coleção

map, em minúsculo, transforma uma coleção. Map, com maiúscula, é o tipo de dicionário. Chamar map sobre um Map devolve uma List, e não outro Map:

package mapa

fun main() {
    val precos = mapOf("maçã" to 3, "banana" to 2)
    println(precos.map { it.value * 2 })
    println(precos.mapValues { it.value * 2 })
}
[6, 4]
{maçã=6, banana=4}

Para continuar com um mapa, use mapValues.

boas práticas

Escreva List, Set e Map nas assinaturas, prefira firstOrNull e ?: quando a ausência é normal, e teste o trecho com uma lista vazia antes de confiar nele.

Pergunta de entrevistaComo você processa uma lista de pedidos em Kotlin com filter, map e sum, e o que observa nesse encadeamento?

Encadeio as operações num pipeline: cada operação devolve uma coleção, e a seguinte recebe essa coleção. Na aula, o filter deixa passar os pedidos de 50 ou mais, o map troca cada par pelo valor e o sum soma o que sobrou, chegando a 400. Dentro da lambda, a desestruturação como (cliente, valor) dá nome às partes do par. O ponto de atenção é a ordem das etapas: o Bruno não aparece no agrupamento porque os pedidos dele ficaram de fora do filtro, então, para contar todos os clientes, agrupo a lista original antes do filtro. Cada etapa cria uma nova coleção, o que é fácil de ler mas aloca resultados intermediários, e em volumes grandes eu avaliaria Sequence.

9. exercícios

Tente cada exercício antes de olhar a resposta.

  1. Dada a lista listOf("Ana" to 120, "Bruno" to 45, "Ana" to 80), imprima o nome do cliente que fez o maior pedido.
  2. Na mesma lista, imprima quantos clientes diferentes existem. Use groupBy.
  3. Uma loja tem mapOf("pão" to 5, "leite" to 6). Imprima o preço do pão e, para o ovo, a mensagem “não encontrado”, sem usar if.

A resposta vem abaixo, com a saída real.

package exercicios

fun main() {
    val pedidos = listOf("Ana" to 120, "Bruno" to 45, "Ana" to 80)
    println(pedidos.sortedBy { it.second }.last().first)
    println(pedidos.groupBy { it.first }.size)

    val precos = mapOf("pão" to 5, "leite" to 6)
    println(precos["pão"] ?: "não encontrado")
    println(precos["ovo"] ?: "não encontrado")
}
Ana
2
5
não encontrado

A primeira resposta ordena pelo valor e pega o último, que é o pedido de 120. A segunda conta as chaves do groupBy, uma por cliente. Na terceira, o pão existe no mapa e o valor é 5, e o ovo não existe, então o ?: usa o texto da direita.

10. glossário

  • Coleção: estrutura que guarda vários valores sob um nome.
  • List: coleção ordenada, que aceita repetidos e é acessada pela posição.
  • Set: coleção sem repetição.
  • Map: coleção de pares chave e valor, com chaves únicas.
  • Par (Pair): dois valores ligados, criados por a to b.
  • Interface somente leitura: tipo que oferece apenas acesso, como List. Não garante que o objeto por trás dela não mude.
  • Lambda: função sem nome, escrita onde é usada e passada como valor.
  • it: nome automático do único parâmetro de uma lambda, quando o tipo pode ser deduzido.
  • Predicado: lambda que devolve Boolean, como a usada por filter.
  • Pipeline: sequência de operações em que cada uma recebe o resultado da anterior.
  • NaN: resultado numérico inválido, como a média de uma lista vazia.

Fontes

Consultadas em 04/11/2026.

Os trechos foram rodados com Kotlin 2.4.21 e OpenJDK 25. As traduções de citações em inglês são livres. O comportamento de first() sem predicado em lista vazia e o NaN de average() foram observados no programa, e não aparecem nas páginas consultadas.

Relacionados

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?