Collections API

The operations reference: transform, filter, aggregate, group, order, and the windowing functions, all in one place.

map / flatMap filter fold / reduce groupBy sortedBy zip / windowed / chunked
๐Ÿ”„

Transform: map & flatMap

map 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.

map, mapIndexed, mapNotNull map
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
flatMap: transform then flatten flatMap
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 List
println(teams.flatMap { it.members })   // [Ada, Grace, Linus]
๐Ÿ”

Filter

Filtering 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.

filter, filterNot, partition filter
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]
any, all, none: yes/no questions any / all / none
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

Aggregation 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.

fold vs reduce fold / reduce
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
Dedicated aggregation functions sumOf / count / maxOrNull
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...
๐Ÿ’ก
Default to 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 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.

groupBy: bucket into a Map of Lists groupBy
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)]
associateBy / associateWith: one entry per element associate*
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 List into 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

Every 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.

sorted, sortedBy, sortedByDescending sorted*
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)]
sortedWith: multi-field comparisons sortedWith
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

These 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.

zip: pairwise combination zip
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]
windowed: sliding sub-lists windowed
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]
chunked: fixed-size, non-overlapping groups chunked
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

Function Signature Shape Notes
maplist.map { transform }Same size, transformed elements
flatMaplist.flatMap { toList }Flattens a list-of-lists into one list
mapNotNulllist.mapNotNull { transform }Transforms and drops nulls in one pass
filter / filterNotlist.filter { predicate }Narrows without transforming
partitionlist.partition { predicate }Splits into (matching, non-matching) in one pass
any / all / nonelist.any { predicate }Boolean result; short-circuits
foldlist.fold(initial) { acc, x -> ... }Explicit seed; safe on empty collections
reducelist.reduce { acc, x -> ... }Seeds from first element; throws if empty
sumOf / count / maxByOrNulllist.sumOf { it.field }Prefer these over hand-written fold when they fit
groupBylist.groupBy { key }Map<Key, List<Element>>, all matches kept
associateBylist.associateBy { key }Map<Key, Element>, last duplicate wins
sortedBylist.sortedBy { key }Ascending by a derived key; returns new list
sortedWithlist.sortedWith(compareBy(...))Multi-field ordering via a Comparator
ziplistA.zip(listB) { a, b -> ... }Pairwise combine; stops at shorter list
windowedlist.windowed(size)Every overlapping sub-list of that size
chunkedlist.chunked(size)Non-overlapping fixed-size groups