Transform: map & flatMap
map ยท flatMap ยท mapNotNull ยท mapIndexedmap applies a transformation to every element, producing a new collection of the same size with different (or differently-typed) contents. flatMap does the same, but expects each transformation to itself produce a collection, and flattens all of those results into a single list โ the fix for the "list of lists" that plain map would otherwise leave you with.
val numbers = listOf(1, 2, 3) println(numbers.map { it * it }) // [1, 4, 9] println(numbers.mapIndexed { i, n -> "$i:$n" }) // [0:1, 1:2, 2:3] // mapNotNull: transform and drop nulls from the result in one pass val raw = listOf("1", "x", "3") println(raw.mapNotNull { it.toIntOrNull() }) // [1, 3]: "x" silently dropped
data class Team(val name: String, val members: List<String>) val teams = listOf( Team("Alpha", listOf("Ada", "Grace")), Team("Beta", listOf("Linus")) ) // map would give List>: [[Ada, Grace], [Linus]]
println(teams.map { it.members }) // [[Ada, Grace], [Linus]] // flatMap flattens the per-team lists into one Listprintln(teams.flatMap { it.members }) // [Ada, Grace, Linus]
Filter
filter ยท filterNot ยท partition ยท any/all/noneFiltering narrows a collection down to the elements matching a predicate, without transforming what survives. Beyond the basic filter/filterNot pair, a few variants save a second pass: partition splits into two lists (matching and non-matching) in one call, and any/all/none answer a yes/no question about the collection instead of returning elements at all.
val numbers = (1..10).toList() println(numbers.filter { it % 2 == 0 }) // [2, 4, 6, 8, 10] println(numbers.filterNot { it % 2 == 0 }) // [1, 3, 5, 7, 9] // partition: two lists in one pass instead of filtering twice val (evens, odds) = numbers.partition { it % 2 == 0 } println(evens) // [2, 4, 6, 8, 10] println(odds) // [1, 3, 5, 7, 9]
val numbers = listOf(2, 4, 6) println(numbers.any { it > 5 }) // true: at least one matches println(numbers.all { it % 2 == 0 }) // true: every element matches println(numbers.none { it < 0 }) // true: no element matches // All three short-circuit: any stops at the first match, // all/none stop at the first counterexample
Aggregate: fold, reduce, sum
fold ยท reduce ยท sumOf ยท countAggregation collapses a collection down to a single value. fold and reduce both walk the collection accumulating a result, but fold takes an explicit starting value (so it works on an empty collection and can produce a different type than the elements), while reduce starts from the first element itself (so it throws on an empty collection, and the result type must match the element type). For the common cases โ sum, count, min, max โ dedicated functions read more directly than a hand-rolled fold.
val numbers = listOf(1, 2, 3, 4) // fold: explicit starting value, can change the result's type val sum = numbers.fold(0) { acc, n -> acc + n } println(sum) // 10 val asText = numbers.fold("Numbers:") { acc, n -> "$acc $n" } println(asText) // "Numbers: 1 2 3 4" // reduce: no starting value, first element seeds the accumulator โ // throws UnsupportedOperationException on an empty collection val product = numbers.reduce { acc, n -> acc * n } println(product) // 24 // emptyList().fold(0) { a, n -> a + n } // fine: returns 0 // emptyList().reduce { a, n -> a + n } // throws: nothing to seed with
data class Order(val total: Double) val orders = listOf(Order(19.99), Order(5.50), Order(42.00)) println(orders.sumOf { it.total }) // 67.49 println(orders.count { it.total > 10 }) // 2 println(orders.maxByOrNull { it.total }) // Order(total=42.0) // No dedicated "averageBy": map to the field first, then average() println(orders.map { it.total }.average()) // 22.496666...
sumOf, count, maxByOrNull, and their relatives over a hand-written fold whenever one of them fits โ they say exactly what they compute in the name, where a fold requires reading the accumulator lambda to understand the intent.Group: groupBy & associate
groupBy ยท associateBy ยท associateWithgroupBy buckets elements into a Map keyed by some derived property, where each value is the list of elements sharing that key โ the direct equivalent of a "GROUP BY" clause. associate and its variants build a Map where each original element becomes exactly one entry, useful for turning a list into a lookup table.
data class Person(val name: String, val city: String) val people = listOf( Person("Ada", "London"), Person("Grace", "New York"), Person("Alan", "London") ) val byCity: Map<String, List<Person>> = people.groupBy { it.city } println(byCity["London"]) // [Person(Ada, London), Person(Alan, London)]
data class User(val id: Int, val name: String) val users = listOf(User(1, "Ada"), User(2, "Grace")) // associateBy: element becomes the VALUE, key derived from it โ // turns a Listinto a lookup Map val byId: Map<Int, User> = users.associateBy { it.id } println(byId[1]) // User(id=1, name=Ada) // associateWith: element becomes the KEY, value derived from it val nameLengths: Map<User, Int> = users.associateWith { it.name.length }
associateBy keeps only the last element for any duplicate key โ earlier entries silently disappear. If duplicate keys are possible and you need every matching element, reach for groupBy instead, which preserves all of them in the value list.Order: sorted & sortedBy
sorted ยท sortedBy ยท sortedWith ยท reversedEvery sort function returns a new, sorted collection rather than mutating the receiver โ the mutating variants (sort, sortBy) exist separately and only apply to a MutableList. sorted works on naturally comparable elements; sortedBy sorts by a derived key; sortedWith takes a full Comparator for anything more elaborate, like multi-field ordering.
data class Person(val name: String, val age: Int) val people = listOf(Person("Grace", 85), Person("Ada", 36)) println(listOf(3, 1, 2).sorted()) // [1, 2, 3]: natural ordering println(people.sortedBy { it.age }) // [Ada(36), Grace(85)] println(people.sortedByDescending { it.age }) // [Grace(85), Ada(36)]
data class Employee(val department: String, val name: String) val employees = listOf( Employee("Eng", "Bob"), Employee("Sales", "Ann"), Employee("Eng", "Amy") ) // compareBy chains multiple sort keys, applied in order val sorted = employees.sortedWith( compareBy({ it.department }, { it.name }) ) // [Employee(Eng, Amy), Employee(Eng, Bob), Employee(Sales, Ann)]
zip, windowed, chunked
pairwise combine ยท sliding window ยท fixed-size groupsThese three cover pairwise and grouped access patterns that don't fit map/filter/fold: zip combines two collections element-by-element into pairs (or a custom combination), windowed produces every sliding sub-list of a given size, and chunked splits a collection into fixed-size, non-overlapping groups.
val names = listOf("Ada", "Grace", "Alan") val ages = listOf(36, 85, 41) // Default: pairs of (name, age); stops at the shorter collection's length println(names.zip(ages)) // [(Ada, 36), (Grace, 85), (Alan, 41)] // With a combining function instead of the default Pair val summaries = names.zip(ages) { name, age -> "$name is $age" } println(summaries) // [Ada is 36, Grace is 85, Alan is 41]
val prices = listOf(10, 12, 9, 15, 11) // Every overlapping window of size 3 println(prices.windowed(3)) // [[10, 12, 9], [12, 9, 15], [9, 15, 11]] // A common use: a moving average val movingAverage = prices.windowed(3) { window -> window.average() } println(movingAverage) // [10.33, 12.0, 11.67]
val numbers = (1..10).toList() // Splits into groups of 3; the last group may be smaller println(numbers.chunked(3)) // [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]] // Common use: batching work โ e.g. sending 100 items at a time to an API numbers.chunked(3).forEach { batch -> println("processing batch: $batch") }
Quick Reference
Syntax cheat-sheet| Function | Signature Shape | Notes |
|---|---|---|
| map | list.map { transform } | Same size, transformed elements |
| flatMap | list.flatMap { toList } | Flattens a list-of-lists into one list |
| mapNotNull | list.mapNotNull { transform } | Transforms and drops nulls in one pass |
| filter / filterNot | list.filter { predicate } | Narrows without transforming |
| partition | list.partition { predicate } | Splits into (matching, non-matching) in one pass |
| any / all / none | list.any { predicate } | Boolean result; short-circuits |
| fold | list.fold(initial) { acc, x -> ... } | Explicit seed; safe on empty collections |
| reduce | list.reduce { acc, x -> ... } | Seeds from first element; throws if empty |
| sumOf / count / maxByOrNull | list.sumOf { it.field } | Prefer these over hand-written fold when they fit |
| groupBy | list.groupBy { key } | Map<Key, List<Element>>, all matches kept |
| associateBy | list.associateBy { key } | Map<Key, Element>, last duplicate wins |
| sortedBy | list.sortedBy { key } | Ascending by a derived key; returns new list |
| sortedWith | list.sortedWith(compareBy(...)) | Multi-field ordering via a Comparator |
| zip | listA.zip(listB) { a, b -> ... } | Pairwise combine; stops at shorter list |
| windowed | list.windowed(size) | Every overlapping sub-list of that size |
| chunked | list.chunked(size) | Non-overlapping fixed-size groups |