Kotlin Collection Operations Explained
Kotlin's collection operations let Charlie replace imperative loops with declarative pipelines — one line replaces 15 lines of for-loop, with clearer intent.
1. What You'll Learn
- Core operators:
map/filter/flatMap/groupBy/associate - Lazy sequences:
Sequenceavoids intermediate collection allocations - Aggregation:
fold/reduce/sumOf/count - Immutable vs mutable:
ListvsMutableList - Charlie in action: processing millions of orders with a functional pipeline
2. A Real Architect's Story
(1) Pain Point: OOM When Processing Millions of Orders
Charlie used List to process millions of order records. Every intermediate operation created a new collection, consuming 3GB of memory and causing OOM.
(2) The Sequence Lazy Evaluation Solution
KOTLIN
// Eager: creates intermediate collections at each step
orders.map { enrich(it) } // Collection 1: 1M elements
.filter { it.total > 100 } // Collection 2: ~500K elements
.toList() // Collection 3
// Lazy: processes one element through entire pipeline
orders.asSequence()
.map { enrich(it) }
.filter { it.total > 100 }
.toList() // Only 1 final collection
Sequence is like an assembly line: each element goes through the entire pipeline before the next one starts — no intermediate collections needed.
3. Core Operators
(1) map — Transform
KOTLIN
val orders = listOf(
Order("ORD-001", 299.99, "Alice"),
Order("ORD-002", 1_500.00, "Bob")
)
// Transform each element
val ids = orders.map { it.id } // [ORD-001, ORD-002]
val summaries = orders.map { "${it.id}: \$${it.total} USD" }
// mapIndexed: with index
orders.mapIndexed { i, order -> "[${i + 1}] ${order.id}" }
// mapNotNull: transform + filter nulls
val emails = orders.mapNotNull { it.customerEmail }
(2) filter — Filter
KOTLIN
// Filter by predicate
val highValue = orders.filter { it.total > 1_000 }
val pending = orders.filter { it.status == "PENDING" }
// filterNot: inverse filter
val active = orders.filterNot { it.status == "CANCELLED" }
// filterIndexed: with index
orders.filterIndexed { i, _ -> i % 2 == 0 } // Even-indexed orders
(3) flatMap — Flatten Transform
KOTLIN
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 + Flatten in one step
val allOrders = customers.flatMap { it.orders }
// [Order(ORD-001, 299.99), Order(ORD-002, 150.0), Order(ORD-003, 1500.0)]
(4) groupBy — Group
KOTLIN
// Group by key
val byCustomer: Map<String, List<Order>> = orders.groupBy { it.customer }
// Group by with value transform
val totalsByCustomer = orders.groupBy(
keySelector = { it.customer },
valueTransform = { it.total }
)
// {Alice=[299.99], Bob=[1500.0]}
(5) associate — Associate to Map
KOTLIN
// Create map from list
val orderMap = orders.associate { it.id to it }
// {ORD-001=Order(...), ORD-002=Order(...)}
// associateBy: key selector
val byId = orders.associateBy { it.id }
// associateBy with value transform
val totalsById = orders.associateBy(
keySelector = { it.id },
valueTransform = { it.total }
)
(6) Operator Quick Reference
| Operator | Function | Input → Output | SQL Analogy |
|---|---|---|---|
map |
Transform | List<A> → List<B> |
SELECT |
filter |
Filter | List<T> → List<T> |
WHERE |
flatMap |
Flatten transform | List<A> → List<B> |
JOIN + SELECT |
groupBy |
Group | List<T> → Map<K, List<T>> |
GROUP BY |
associate |
Map | List<T> → Map<K, V> |
- |
distinct |
Deduplicate | List<T> → List<T> |
DISTINCT |
sortedBy |
Sort | List<T> → List<T> |
ORDER BY |
4. Collection Operation Pipeline
flowchart LR
A[Orders<br/>1M records] --> B[filter<br/>total > 1000]
B --> C[map<br/>extract customer]
C --> D[distinct<br/>unique customers]
D --> E[groupBy<br/>by region]
E --> F[Result<br/>Map of customers]
5. Sequence Lazy Evaluation
(1) Eager vs Lazy
KOTLIN
// Eager (List): each step creates new collection
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()
// Lazy (Sequence): processes one element at a time
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 only processes elements until take(2) is satisfied
(2) When to Use Sequence
| Scenario | Use List | Use Sequence |
|---|---|---|
| Data size | < 10,000 | > 10,000 |
| Operation steps | 1-2 steps | 3+ steps |
| Intermediate result size | Close to source size | Significantly reduced |
| Multiple traversals needed | Yes | No |
(3) List vs Sequence Performance
| Dimension | List (Eager) | Sequence (Lazy) |
|---|---|---|
| Intermediate collections | Created at every step | Not created |
| Memory | O(n × steps) | O(1) |
| Short-circuit operations | Not optimized | Optimized (e.g., take only processes N) |
| First result | Waits for full pipeline | Available immediately |
6. Aggregation Operations
(1) fold / reduce
KOTLIN
// fold: with initial value
val totalRevenue = orders.fold(0.0) { acc, order -> acc + order.total }
// reduce: first element as initial value
val maxOrder = orders.reduce { max, order ->
if (order.total > max.total) order else max
}
// foldRight: from end to start
val reversed = orders.foldRight(emptyList<Order>()) { order, acc -> acc + order }
(2) Convenience Aggregation
KOTLIN
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 }
// Sorting
val sorted = orders.sortedByDescending { it.total }
val top3 = orders.sortedByDescending { it.total }.take(3)
(3) fold vs reduce
| Dimension | fold |
reduce |
|---|---|---|
| Initial value | Must provide | Implicit first element |
| Empty collection | Safe (returns initial value) | Throws exception |
| Return type | Can differ from element type | Same as element type |
| Recommendation | ⭐⭐⭐ | ⭐⭐ |
7. Immutable vs Mutable Collections
(1) Read-Only and Mutable Interfaces
KOTLIN
// Read-only (immutable interface)
val list: List<Order> = listOf(Order("ORD-001", 299.99, "Alice"))
// Mutable
val mutableList: MutableList<Order> = mutableListOf()
mutableList.add(Order("ORD-002", 1_500.00, "Bob"))
// Read-only view of mutable list
val readOnly: List<Order> = mutableList // OK: MutableList extends List
// readOnly.add(...) // ERROR: List has no add method
mutableList.add(Order("ORD-003", 45.50, "Charlie")) // Changes readOnly view!
(2) Defensive Copy
KOTLIN
class OrderProcessor(private val _orders: MutableList<Order>) {
// Defensive copy: expose immutable view
val orders: List<Order> get() = _orders.toList()
// Or use immutable view (no copy, but can be cast back)
val ordersView: List<Order> get() = _orders.toList()
}
(3) Collection Type Comparison
| Type | Read-Only Interface | Mutable Interface | Factory Functions |
|---|---|---|---|
| List | List |
MutableList |
listOf / mutableListOf |
| Set | Set |
MutableSet |
setOf / mutableSetOf |
| Map | Map |
MutableMap |
mapOf / mutableMapOf |
8. Complete Example: Million-Order Processing Pipeline
KOTLIN
// ============================================
// OrderProcessor - Collection Pipeline
// Feature: Process orders with functional operators
// ============================================
data class Order(val id: String, val total: Double, val status: String, val customer: String, val region: String)
fun main() {
// Simulate order data
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: High-value confirmed orders
println("=== High-Value Confirmed Orders ===")
orders.filter { it.status == "CONFIRMED" && it.total > 1_000 }
.sortedByDescending { it.total }
.forEach { println(" ${it.id}: \$${it.total} USD (${it.customer})") }
// Pipeline 2: Revenue by region
println("\n=== Revenue by Region ===")
orders.filter { it.status != "CANCELLED" }
.groupBy { it.region }
.mapValues { (_, list) -> list.sumOf { it.total } }
.forEach { (region, revenue) -> println(" $region: \$$revenue USD") }
// Pipeline 3: Top customers by order count and revenue
println("\n=== Customer Summary ===")
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 orders, \$${total} USD total, \$${"%.2f".format(avg)} avg"
}
.forEach { println(" $it") }
// Pipeline 4: Using Sequence for efficient processing
println("\n=== Top 3 Orders (Sequence) ===")
orders.asSequence()
.filter { it.status != "CANCELLED" }
.sortedByDescending { it.total }
.take(3)
.forEach { println(" ${it.id}: \$${it.total} USD") }
// Aggregate: fold to build a summary string
val summary = orders
.filter { it.status != "CANCELLED" }
.fold("Order Summary: ") { acc, order -> "$acc\n ${order.id} (\$${order.total} USD)" }
println("\n${summary}")
println("Total orders: ${orders.size}, Active: ${orders.count { it.status != "CANCELLED" }}")
}
Output:
TEXT
=== High-Value Confirmed Orders ===
ORD-005: $8900.0 USD (Bob)
ORD-002: $15000.0 USD (Bob)
=== Revenue by Region ===
US: $7599.99 USD
EU: $46400.0 USD
ASIA: $350.0 USD
=== Customer Summary ===
Alice: 2 orders, $3649.99 USD total, $1824.995 avg
Bob: 3 orders, $46400.0 USD total, $15466.666666666666 avg
Charlie: 3 orders, $4450.0 USD total, $1483.3333333333333 avg
=== Top 3 Orders (Sequence) ===
ORD-008: $22000.0 USD (Bob)
ORD-002: $15000.0 USD (Bob)
ORD-005: $8900.0 USD (Bob)
Order Summary:
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 orders: 10, Active: 9
❓ FAQ
Q What's the difference between Kotlin collection operations and Java Stream?
A Kotlin collection operations are more concise (
it keyword, trailing lambdas), no need for stream()/collect() conversions, and Sequence is similar to Stream but lighter.Q When should I use Sequence?
A Use Sequence when data is large (>10,000), there are many operation steps (3+), or you need short-circuiting (
take/first). For small data, List is simpler.Q Does
toList() copy data?A Yes.
toList() creates a new list — it's a defensive copy. If you only need a read-only view without a copy, just use a List interface reference.Q fold or reduce — which should I choose?
A Prefer
fold because it allows specifying an initial value and is safe for empty collections. reduce throws an exception on empty collections.Q Is Kotlin's List truly immutable?
A No. Kotlin's
List is a read-only interface (cannot be modified through it), but the underlying implementation may be mutable. True immutability requires creating a copy with toList().Q Is there a difference between
flatMap and map + flatten?A Functionally equivalent;
flatMap is more efficient (done in one step). It's the combined shorthand for map + flatten.📖 Summary
- Core operators:
map(transform),filter(filter),flatMap(flatten transform),groupBy(group),associate(to map) - Sequence lazy evaluation avoids intermediate collection allocation — preferred for large datasets
fold(has initial value, safe) is preferred overreduce(unsafe on empty collections)Listis a read-only interface,MutableListis mutable — prefer read-only- Defensive copy with
toList()protects internal mutable state - Collection operation pipelines make data processing code declarative, readable, and composable
📝 Exercises
- Beginner (⭐): Use
filterandmapto extract IDs of orders with amounts > 1000 USD from an order list. Hint:orders.filter { }.map { } - Intermediate (⭐⭐): Use
groupBy+mapValuesto calculate each customer's total spending, sorted by amount in descending order. Hint:groupBy { it.customer }.mapValues { } - Advanced (⭐⭐⭐): Use
Sequenceto implement a lazy processing pipeline for millions of orders: filter → map → take(100), and print the number of elements actually processed. Hint: use a counter inonEach



