Kotlin: Operações de Coleção do Kotlin Explicadas
Última atualização: 2026-08-26
As operações de coleção do Kotlin permitem que Charlie substitua loops imperativos por pipelines declarativos — uma linha substitui 15 linhas de loop for, com intenção mais clara.
1. O que Você Aprenderá
- Operadores principais:
map/filter/flatMap/groupBy/associate - Sequências preguiçosas:
Sequenceevita alocações de coleções intermediárias - Agregação:
fold/reduce/sumOf/count - Imutável vs mutável:
ListvsMutableList - Charlie em ação: processando milhões de pedidos com um pipeline funcional
2. A História Real de um Arquiteto
(1) Ponto de Dor: OOM ao Processar Milhões de Pedidos
Charlie usou List para processar milhões de registros de pedidos. Cada operação intermediária criava uma nova coleção, consumindo 3GB de memória e causando OOM.
(2) A Solução com Avaliação Preguiçosa do Sequence
// Eager: cria coleções intermediárias em cada etapa
orders.map { enrich(it) } // Coleção 1: 1M elementos
.filter { it.total > 100 } // Coleção 2: ~500K elementos
.toList() // Coleção 3
// Preguiçoso: processa um elemento por todo o pipeline
orders.asSequence()
.map { enrich(it) }
.filter { it.total > 100 }
.toList() // Apenas 1 coleção final
Sequence é como uma linha de montagem: cada elemento passa por todo o pipeline antes do próximo começar — sem coleções intermediárias necessárias.
3. Operadores Principais
(1) map — Transformar
val orders = listOf(
Order("ORD-001", 299.99, "Alice"),
Order("ORD-002", 1_500.00, "Bob")
)
// Transformar cada elemento
val ids = orders.map { it.id } // [ORD-001, ORD-002]
val summaries = orders.map { "${it.id}: \$${it.total} USD" }
// mapIndexed: com índice
orders.mapIndexed { i, order -> "[${i + 1}] ${order.id}" }
// mapNotNull: transformar + filtrar nulos
val emails = orders.mapNotNull { it.customerEmail }
(2) filter — Filtrar
// Filtrar por predicado
val highValue = orders.filter { it.total > 1_000 }
val pending = orders.filter { it.status == "PENDING" }
// filterNot: filtro inverso
val active = orders.filterNot { it.status == "CANCELLED" }
// filterIndexed: com índice
orders.filterIndexed { i, _ -> i % 2 == 0 } // Pedidos de índice par
(3) flatMap — Transformar e Achatar
data class Customer(val name: String, val orders: List<Order>)
val customers = listOf(
Customer("Alice", listOf(Order("ORD-001", 299.99), Order("ORD-002", 150.0))),
Customer("Bob", listOf(Order("ORD-003", 1_500.00)))
)
// Map + Achatar em um passo
val allOrders = customers.flatMap { it.orders }
// [Order(ORD-001, 299.99), Order(ORD-002, 150.0), Order(ORD-003, 1500.0)]
(4) groupBy — Agrupar
// Agrupar por chave
val byCustomer: Map<String, List<Order>> = orders.groupBy { it.customer }
// Agrupar por com transformação de valor
val totalsByCustomer = orders.groupBy(
keySelector = { it.customer },
valueTransform = { it.total }
)
// {Alice=[299.99], Bob=[1500.0]}
(5) associate — Associar a Map
// Criar map a partir de lista
val orderMap = orders.associate { it.id to it }
// {ORD-001=Order(...), ORD-002=Order(...)}
// associateBy: seletor de chave
val byId = orders.associateBy { it.id }
// associateBy com transformação de valor
val totalsById = orders.associateBy(
keySelector = { it.id },
valueTransform = { it.total }
)
(6) Referência Rápida de Operadores
| Operador | Função | Entrada → Saída | Analogia SQL |
|---|---|---|---|
map |
Transformar | List<A> → List<B> |
SELECT |
filter |
Filtrar | List<T> → List<T> |
WHERE |
flatMap |
Transformar e achatar | List<A> → List<B> |
JOIN + SELECT |
groupBy |
Agrupar | List<T> → Map<K, List<T>> |
GROUP BY |
associate |
Mapear | List<T> → Map<K, V> |
- |
distinct |
Deduplicar | List<T> → List<T> |
DISTINCT |
sortedBy |
Ordenar | List<T> → List<T> |
ORDER BY |
4. Pipeline de Operações de Coleção
flowchart LR
A[Pedidos<br/>1M registros] --> B[filter<br/>total > 1000]
B --> C[map<br/>extrair cliente]
C --> D[distinct<br/>clientes únicos]
D --> E[groupBy<br/>por região]
E --> F[Resultado<br/>Map de clientes]
5. Avaliação Preguiçosa com Sequence
(1) Eager vs Preguiçoso
// Eager (List): cada etapa cria nova coleção
val result = orders
.map { println("map: ${it.id}"); it.copy(total = it.total * 0.9) }
.filter { println("filter: ${it.id}"); it.total > 100 }
.take(2)
.toList()
// Preguiçoso (Sequence): processa um elemento por vez
val result2 = orders.asSequence()
.map { println("seq-map: ${it.id}"); it.copy(total = it.total * 0.9) }
.filter { println("seq-filter: ${it.id}"); it.total > 100 }
.take(2)
.toList()
// Sequence só processa elementos até que take(2) seja satisfeito
(2) Quando Usar Sequence
| Cenário | Usar List | Usar Sequence |
|---|---|---|
| Tamanho dos dados | < 10.000 | > 10.000 |
| Etapas de operação | 1-2 etapas | 3+ etapas |
| Tamanho do resultado intermediário | Próximo ao tamanho original | Significativamente reduzido |
| Múltiplas travessias necessárias | Sim | Não |
(3) Desempenho List vs Sequence
| Dimensão | List (Eager) | Sequence (Preguiçoso) |
|---|---|---|
| Coleções intermediárias | Criadas em cada etapa | Não criadas |
| Memória | O(n × etapas) | O(1) |
| Operações de curto-circuito | Não otimizadas | Otimizadas (ex.: take só processa N) |
| Primeiro resultado | Espera pipeline completo | Disponível imediatamente |
6. Operações de Agregação
(1) fold / reduce
// fold: com valor inicial
val totalRevenue = orders.fold(0.0) { acc, order -> acc + order.total }
// reduce: primeiro elemento como valor inicial
val maxOrder = orders.reduce { max, order ->
if (order.total > max.total) order else max
}
// foldRight: do final ao início
val reversed = orders.foldRight(emptyList<Order>()) { order, acc -> acc + order }
(2) Agregação de Conveniência
val total = orders.sumOf { it.total }
val avg = orders.map { it.total }.average()
val max = orders.maxByOrNull { it.total }
val min = orders.minByOrNull { it.total }
val count = orders.count { it.total > 1_000 }
// Ordenação
val sorted = orders.sortedByDescending { it.total }
val top3 = orders.sortedByDescending { it.total }.take(3)
(3) fold vs reduce
| Dimensão | fold |
reduce |
|---|---|---|
| Valor inicial | Deve fornecer | Primeiro elemento implícito |
| Coleção vazia | Seguro (retorna valor inicial) | Lança exceção |
| Tipo de retorno | Pode diferir do tipo do elemento | Mesmo tipo do elemento |
| Recomendação | ⭐⭐⭐ | ⭐⭐ |
7. Coleções Imutáveis vs Mutáveis
(1) Interfaces Somente Leitura e Mutáveis
// Somente leitura (interface imutável)
val list: List<Order> = listOf(Order("ORD-001", 299.99, "Alice"))
// Mutável
val mutableList: MutableList<Order> = mutableListOf()
mutableList.add(Order("ORD-002", 1_500.00, "Bob"))
// Visão somente leitura de lista mutável
val readOnly: List<Order> = mutableList // OK: MutableList estende List
// readOnly.add(...) // ERRO: List não tem método add
mutableList.add(Order("ORD-003", 45.50, "Charlie")) // Muda a visão readOnly!
(2) Cópia Defensiva
class OrderProcessor(private val _orders: MutableList<Order>) {
// Cópia defensiva: expor visão imutável
val orders: List<Order> get() = _orders.toList()
// Ou usar visão imutável (sem cópia, mas pode ser convertido de volta)
val ordersView: List<Order> get() = _orders.toList()
}
(3) Comparação de Tipos de Coleção
| Tipo | Interface Somente Leitura | Interface Mutável | Funções de Fábrica |
|---|---|---|---|
| List | List |
MutableList |
listOf / mutableListOf |
| Set | Set |
MutableSet |
setOf / mutableSetOf |
| Map | Map |
MutableMap |
mapOf / mutableMapOf |
8. Exemplo Completo: Pipeline de Processamento de Milhões de Pedidos
// ============================================
// OrderProcessor - Pipeline de Coleção
// Recurso: Processar pedidos com operadores funcionais
// ============================================
data class Order(val id: String, val total: Double, val status: String, val customer: String, val region: String)
fun main() {
// Simular dados de pedidos
val orders = listOf(
Order("ORD-001", 299.99, "CONFIRMED", "Alice", "US"),
Order("ORD-002", 15_000.00, "CONFIRMED", "Bob", "EU"),
Order("ORD-003", 2_500.00, "PENDING", "Charlie", "US"),
Order("ORD-004", 45.50, "CANCELLED", "Alice", "ASIA"),
Order("ORD-005", 8_900.00, "CONFIRMED", "Bob", "EU"),
Order("ORD-006", 1_200.00, "SHIPPED", "Charlie", "US"),
Order("ORD-007", 350.00, "CONFIRMED", "Alice", "ASIA"),
Order("ORD-008", 22_000.00, "PENDING", "Bob", "EU"),
Order("ORD-009", 750.00, "CONFIRMED", "Charlie", "US"),
Order("ORD-010", 4_500.00, "SHIPPED", "Alice", "US")
)
// Pipeline 1: Pedidos confirmados de alto valor
println("=== Pedidos Confirmados de Alto Valor ===")
orders.filter { it.status == "CONFIRMED" && it.total > 1_000 }
.sortedByDescending { it.total }
.forEach { println(" ${it.id}: \$${it.total} USD (${it.customer})") }
// Pipeline 2: Receita por região
println("\n=== Receita por Região ===")
orders.filter { it.status != "CANCELLED" }
.groupBy { it.region }
.mapValues { (_, list) -> list.sumOf { it.total } }
.forEach { (region, revenue) -> println(" $region: \$$revenue USD") }
// Pipeline 3: Principais clientes por contagem de pedidos e receita
println("\n=== Resumo de Clientes ===")
orders.filter { it.status != "CANCELLED" }
.groupBy { it.customer }
.map { (customer, list) ->
val count = list.size
val total = list.sumOf { it.total }
val avg = list.map { it.total }.average()
"$customer: $count pedidos, \$${total} USD total, \$${"%.2f".format(avg)} média"
}
.forEach { println(" $it") }
// Pipeline 4: Usando Sequence para processamento eficiente
println("\n=== Top 3 Pedidos (Sequence) ===")
orders.asSequence()
.filter { it.status != "CANCELLED" }
.sortedByDescending { it.total }
.take(3)
.forEach { println(" ${it.id}: \$${it.total} USD") }
// Agregação: fold para construir string de resumo
val summary = orders
.filter { it.status != "CANCELLED" }
.fold("Resumo de Pedidos: ") { acc, order -> "$acc\n ${order.id} (\$${order.total} USD)" }
println("\n${summary}")
println("Total de pedidos: ${orders.size}, Ativos: ${orders.count { it.status != "CANCELLED" }}")
}
Saída:
=== Pedidos Confirmados de Alto Valor ===
ORD-005: $8900.0 USD (Bob)
ORD-002: $15000.0 USD (Bob)
=== Receita por Região ===
US: $7599.99 USD
EU: $46400.0 USD
ASIA: $350.0 USD
=== Resumo de Clientes ===
Alice: 2 pedidos, $3649.99 USD total, $1824.995 média
Bob: 3 pedidos, $46400.0 USD total, $15466.666666666666 média
Charlie: 3 pedidos, $4450.0 USD total, $1483.3333333333333 média
=== Top 3 Pedidos (Sequence) ===
ORD-008: $22000.0 USD (Bob)
ORD-002: $15000.0 USD (Bob)
ORD-005: $8900.0 USD (Bob)
Resumo de Pedidos:
ORD-001 ($299.99 USD)
ORD-002 ($15000.0 USD)
ORD-003 ($2500.0 USD)
ORD-005 ($8900.0 USD)
ORD-006 ($1200.0 USD)
ORD-007 ($350.0 USD)
ORD-008 ($22000.0 USD)
ORD-009 ($750.0 USD)
ORD-010 ($4500.0 USD)
Total de pedidos: 10, Ativos: 9
9. Exemplos práticos rápidos
▶ Exemplo: List imutável e mutável
// List imutável (apenas leitura)
val immutableList = listOf("ORD-001", "ORD-002", "ORD-003")
println("Imutável: $immutableList")
// immutableList.add("ORD-004") // erro de compilação
// List mutável
val mutableList = mutableListOf("a", "b", "c")
mutableList.add("d")
mutableList.removeAt(0)
mutableList[0] = "AA"
println("Mutável: $mutableList")
// Operações similares a set
val seen = mutableSetOf<Int>()
listOf(1, 2, 2, 3, 3, 3, 4).forEach { seen.add(it) }
println("Únicos: $seen")
Saída:
Imutável: [ORD-001, ORD-002, ORD-003]
Mutável: [AA, c, d]
Únicos: [1, 2, 3, 4]
▶ Exemplo: Map básico
// Map imutável
val prices = mapOf(
"ORD-001" to 299.99,
"ORD-002" to 1500.00,
"ORD-003" to 50.00
)
println("Preço de ORD-001: ${prices["ORD-001"]}")
println("Preço de ORD-999: ${prices["ORD-999"] ?: 0.0}")
// Iteração
prices.forEach { (id, price) -> println("$id -> \$$price") }
// Map mutável
val cart = mutableMapOf<String, Int>()
cart["apple"] = 3
cart["banana"] = 5
println("Carrinho: $cart")
// Operações atomicas
cart.merge("apple", 2) { old, new -> old + new }
println("Após merge: $cart")
Saída:
Preço de ORD-001: 299.99
Preço de ORD-999: 0.0
ORD-001 -> $299.99
ORD-002 -> $1500.0
ORD-003 -> $50.0
Carrinho: {apple=3, banana=5}
Após merge: {apple=5, banana=5}
▶ Exemplo: filter, map, flatMap
val orders = listOf(
mapOf("id" to "ORD-001", "amount" to 299, "status" to "CONFIRMED"),
mapOf("id" to "ORD-002", "amount" to 1500, "status" to "PENDING"),
mapOf("id" to "ORD-003", "amount" to 50, "status" to "CANCELLED"),
mapOf("id" to "ORD-004", "amount" to 2300, "status" to "CONFIRMED"),
mapOf("id" to "ORD-005", "amount" to 500, "status" to "PENDING")
)
// Filtro
val confirmed = orders.filter { it["status"] == "CONFIRMED" }
println("Confirmados: ${confirmed.map { it["id"] }}")
// Map - projetar
val ids = orders.map { it["id"] }
println("Todos IDs: $ids")
// Filter + map
val confirmedAmounts = orders.filter { it["status"] == "CONFIRMED" }
.map { it["amount"] as Int * 1.08 }
println("Valores confirmados c/ imposto: $confirmedAmounts")
// flatMap
val users = listOf(
mapOf("name" to "Alice", "roles" to listOf("admin", "user")),
mapOf("name" to "Bob", "roles" to listOf("user"))
)
val allRoles = users.flatMap { it["roles"] as List<String> }
println("Todos papéis: $allRoles")
Saída:
Confirmados: [ORD-001, ORD-004]
Todos IDs: [ORD-001, ORD-002, ORD-003, ORD-004, ORD-005]
Valores confirmados c/ imposto: [323, 2484]
Todos papéis: [admin, user, user]
▶ Exemplo: reduce, fold, groupBy
val numbers = listOf(1, 2, 3, 4, 5)
// reduce - acumulação, sem valor inicial
val product = numbers.reduce { acc, n -> acc * n }
println("Produto: $product")
// fold - com valor inicial (permite tipo diferente)
val sumAsString = numbers.fold("0") { acc, n -> "$acc + $n" }
println("Soma como string: $sumAsString = ${numbers.sum()}")
// groupBy
val words = listOf("apple", "banana", "avocado", "blueberry", "cherry")
val byFirstLetter = words.groupBy { it[0] }
byFirstLetter.forEach { (letter, list) ->
println("$letter: $list")
}
Saída:
Produto: 120
Soma como string: 0 + 1 + 2 + 3 + 4 + 5 = 15
a: [apple, avocado]
b: [banana, blueberry]
c: [cherry]
▶ Exemplo: aggregate e sumBy
data class Sale(val product: String, val amount: Double)
val sales = listOf(
Sale("Coffee", 4.50),
Sale("Tea", 3.00),
Sale("Cake", 8.00),
Sale("Cookie", 2.50)
)
// sumOf
val total = sales.sumOf { it.amount }
println("Total: \$$total")
// sumOf com transformação diferente
val totalCents = sales.sumOf { (it.amount * 100).toInt() }
println("Total em centavos: $totalCents")
// Média
val avg = sales.map { it.amount }.average()
println("Média: \$$avg")
// Min/Max
val min = sales.minByOrNull { it.amount }
val max = sales.maxByOrNull { it.amount }
println("Mínimo: $min")
println("Máximo: $max")
Saída:
Total: $18.0
Total em centavos: 1800
Média: $4.5
Mínimo: Sale(product=Cookie, amount=2.5)
Máximo: Sale(product=Cake, amount=8.0)
▶ Exemplo: Sequence vs List
// List: eager - cada operação cria uma lista intermediária
val numbers = (1..10_000_000).toList()
val timeList = System.currentTimeMillis()
val sumList = numbers
.filter { it % 2 == 0 }
.map { it * 2 }
.take(5)
.sum()
val durationList = System.currentTimeMillis() - timeList
// Sequence: lazy - processo item por item, sem coleções intermediárias
val timeSeq = System.currentTimeMillis()
val sumSeq = numbers.asSequence()
.filter { it % 2 == 0 }
.map { it * 2 }
.take(5)
.sum()
val durationSeq = System.currentTimeMillis() - timeSeq
println("Soma (List): $sumList em ${durationList}ms")
println("Soma (Sequence): $sumSeq em ${durationSeq}ms")
// Quando usar cada um:
// - List: pequeno dataset, muitas chamadas terminais
// - Sequence: grande dataset, várias transformações em cadeia
Saída:
Soma (List): 120 em XXms
Soma (Sequence): 120 em YYms
▶ Exemplo: zip, partition, chunked
val names = listOf("Ana", "Bruno", "Carla")
val scores = listOf(85, 92, 78)
val grades = listOf("B", "A", "C")
// zip: combinar duas listas
val combined = names.zip(scores)
println("Combinado: $combined")
// zip com transform
val summary = names.zip(scores) { n, s -> "$n: $s" }
println("Resumo: $summary")
// Três listas combinadas: zipTransversal
val fullReport = names.zip(scores).zip(grades) { pair, g ->
"${pair.first}: ${pair.second} ($g)"
}
println("Relatório: $fullReport")
// chunked: dividir em grupos
val nums = (1..10).toList()
val chunks = nums.chunked(3)
println("Chunks: $chunks")
// windowed: janelas deslizantes
val threeSum = nums.windowed(3) { it.sum() }
println("Soma de janelas: $threeSum")
Saída:
Combinado: [(Ana, 85), (Bruno, 92), (Carla, 78)]
Resumo: [Ana: 85, Bruno: 92, Carla: 78]
Relatório: [Ana: 85 (B), Bruno: 92 (A), Carla: 78 (C)]
Chunks: [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
Soma de janelas: [6, 9, 12, 15, 18, 21, 24, 27]
❓ Perguntas Frequentes
P: Qual a diferença entre operações de coleção do Kotlin e Java Stream? R: Operações de coleção do Kotlin são mais concisas (palavra-chave
it, lambdas finais), sem necessidade de conversõesstream()/collect(), eSequenceé similar ao Stream, porém mais leve.
P: Quando devo usar Sequence? R: Use Sequence quando os dados são grandes (>10.000), há muitas etapas de operação (3+), ou você precisa de curto-circuito (
take/first). Para dados pequenos, List é mais simples.
P:
toList()copia dados? R: Sim.toList()cria uma nova lista — é uma cópia defensiva. Se você precisa apenas de uma visão somente leitura sem cópia, basta usar uma referência de interfaceList.
P: fold ou reduce — qual escolher? R: Prefira
foldporque permite especificar um valor inicial e é seguro para coleções vazias.reducelança exceção em coleções vazias.
P: A List do Kotlin é verdadeiramente imutável? R: Não. A
Listdo Kotlin é uma interface somente leitura (não pode ser modificada através dela), mas a implementação subjacente pode ser mutável. Verdadeira imutabilidade requer criar uma cópia comtoList().
P: Há diferença entre
flatMapemap+flatten? R: Funcionalmente equivalentes;flatMapé mais eficiente (feito em um passo). É a abreviação combinada demap+flatten.
📖 Resumo
- Operadores principais:
map(transformar),filter(filtrar),flatMap(achatar e transformar),groupBy(agrupar),associate(para map) - Avaliação preguiçosa com Sequence evita alocação de coleções intermediárias — preferida para grandes conjuntos de dados
fold(tem valor inicial, seguro) é preferido sobrereduce(inseguro em coleções vazias)Listé uma interface somente leitura,MutableListé mutável — prefira somente leitura- Cópia defensiva com
toList()protege estado mutável interno - Pipelines de operações de coleção tornam o código de processamento de dados declarativo, legível e composável
📝 Exercícios
- Iniciante (⭐): Use
filteremappara extrair IDs de pedidos com valores > 1000 USD de uma lista de pedidos. Dica:orders.filter { }.map { } - Intermediário (⭐⭐): Use
groupBy+mapValuespara calcular o gasto total de cada cliente, ordenado por valor em ordem decrescente. Dica:groupBy { it.customer }.mapValues { } - Avançado (⭐⭐⭐): Use
Sequencepara implementar um pipeline de processamento preguiçoso para milhões de pedidos: filter → map → take(100), e imprima o número de elementos realmente processados. Dica: use um contador emonEach