Kotlin’s collections plus its functional operators — map, filter, reduce, groupBy — let you transform data declaratively in readable chains instead of manual loops. Plus sequences for large or lazy pipelines.
Kotlin separates read-only collections (listOf, mapOf) from mutable ones (mutableListOf), defaulting you to immutable, which is safer. The three core types cover most needs: ordered lists, unique-element sets, and key-value maps. Preferring the read-only interfaces communicates intent and prevents accidental modification — the same immutability theme as val.
fun main() {
val nums = listOf(1, 2, 3, 2) // read-only List
val unique = setOf(1, 2, 3, 2) // Set -> {1, 2, 3}
val ages = mapOf("Ada" to 36, "Bo" to 25) // Map
println(nums[0]) // 1 (indexed access)
println("Ada" in ages) // true
println(ages["Bo"]) // 25
val mutable = mutableListOf(1, 2) // opt into mutability explicitly
mutable.add(3)
// prefer read-only (listOf) unless you truly need to mutate.
}Instead of writing loops, you transform collections declaratively with chained operators: filter keeps matching elements, map transforms each, and dozens more (sortedBy, take, distinct, any, count) compose into readable pipelines. This functional style is clearer and less error-prone than manual iteration, and it is how idiomatic Kotlin — and the collections behind Android UIs — is written.
data class User(val name: String, val age: Int, val active: Boolean)
fun main() {
val users = listOf(
User("Ada", 36, true), User("Bo", 17, true), User("Cy", 42, false)
)
val names = users
.filter { it.active } // keep active users
.filter { it.age >= 18 } // keep adults
.sortedBy { it.age } // sort by age
.map { it.name } // extract names
println(names) // [Ada]
println(users.any { it.age < 18 }) // true
println(users.count { it.active }) // 2
}Beyond map/filter, a few operators handle aggregation elegantly. groupBy partitions elements into a map by a key, associateBy builds a lookup map, and fold/reduce collapse a collection to a single value with an accumulator. These replace the verbose accumulator loops you would otherwise write and express the intent directly.
data class Sale(val region: String, val amount: Int)
fun main() {
val sales = listOf(
Sale("EU", 100), Sale("US", 200), Sale("EU", 50), Sale("US", 75)
)
// group into a Map<String, List<Sale>>
val byRegion = sales.groupBy { it.region }
// total per region: Map<String, Int>
val totals = sales.groupBy { it.region }
.mapValues { (_, list) -> list.sumOf { it.amount } }
println(totals) // {EU=150, US=275}
// fold: collapse to one value with an accumulator
val grandTotal = sales.fold(0) { acc, s -> acc + s.amount }
println(grandTotal) // 425
}Chained collection operators are eager: each step creates a new intermediate list. For large data or long chains, converting to a sequence makes the pipeline lazy — elements flow through all operations one at a time, and short-circuiting operations like first() stop early. It is the same code with asSequence(), trading eager simplicity for efficiency when it matters.
fun main() {
val nums = (1..1_000_000)
// EAGER: each operator builds a full intermediate list (wasteful here)
// val r = nums.map { it * 2 }.filter { it % 3 == 0 }.first()
// LAZY: elements flow through one at a time; 'first()' stops early
val r = nums.asSequence()
.map { it * 2 }
.filter { it % 3 == 0 }
.first() // computes just enough to find one
println(r) // 6
// use sequences for large data or long chains; plain lists for small ones.
}