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
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.
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.
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.
- Dada a lista
listOf("Ana" to 120, "Bruno" to 45, "Ana" to 80), imprima o nome do cliente que fez o maior pedido. - Na mesma lista, imprima quantos clientes diferentes existem. Use
groupBy. - 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 usarif.
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 porfilter. - 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.
- Visão geral das coleções: https://kotlinlang.org/docs/collections-overview.html
- Lambdas: https://kotlinlang.org/docs/lambdas.html
- Operações sobre coleções: https://kotlinlang.org/docs/collection-operations.html
- Transformações (map, mapValues): https://kotlinlang.org/docs/collection-transformations.html
- Filtragem (filter, any, all): https://kotlinlang.org/docs/collection-filtering.html
- Elementos individuais (first, firstOrNull, find): https://kotlinlang.org/docs/collection-elements.html
- Agregados (sum, count, average): https://kotlinlang.org/docs/collection-aggregate.html
- Agrupamento (groupBy): https://kotlinlang.org/docs/collection-grouping.html
- Ordenação (sortedBy): https://kotlinlang.org/docs/collection-ordering.html
- Referência da API, average para Iterable de Int: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/average.html
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.