Kotlin collection operators mental map
This page converts the shared PDF into a searchable web lesson. The PDF is still available here: Download the Kotlin Collection Operators mental map.
Do not start by memorizing function names. First ask what problem you are solving: keep values, change values, find one value, check a condition, group data, sort data, combine data, or cut data into batches.
The complete mental map
| Question | Operators |
|---|---|
| Which elements do I want? | filter, filterNot, filterIndexed, filterNotNull, filterIsInstance, partition |
| How do I cut from the start or end? | take, takeWhile, takeLast, takeLastWhile, drop, dropWhile, dropLast, dropLastWhile |
| How do I remove duplicates? | distinct, distinctBy |
| What should each element become? | map, mapIndexed, mapNotNull, mapIndexedNotNull |
| How do I flatten nested collections? | flatMap, flatMapIndexed, flatten |
| How do I get one element? | first, firstOrNull, last, lastOrNull, single, singleOrNull, find, findLast |
| How do I access by position? | get, [index], getOrNull, elementAt, elementAtOrNull, elementAtOrElse |
| How do I pick randomly? | random, randomOrNull |
| How do I check conditions? | any, all, none |
| How do I count or calculate? | count, sum, sumOf, average, minOrNull, maxOrNull, minByOrNull, maxByOrNull, fold, reduce |
| How do I sort? | sorted, sortedDescending, sortedBy, sortedByDescending, sortedWith, reversed |
| How do I create maps or groups? | groupBy, groupingBy, eachCount, associate, associateBy, associateWith |
| How do I combine collections? | zip, unzip, zipWithNext, plus, minus |
| How do I create batches/windows? | chunked, windowed, windowedSequence, runningFold, runningReduce |
Example model used by the labs
data class Student(
val name: String,
val age: Int,
val courses: List<String>,
val marks: Int
)
fun sampleStudents() = listOf(
Student("Arun", 22, listOf("Kotlin", "Android"), 80),
Student("Priya", 24, listOf("Java", "Kotlin"), 90),
Student("Rahul", 21, listOf("Android", "Flutter"), 70),
Student("Meena", 23, listOf("Kotlin", "Compose"), 85)
)
Filtering and slicing
Filtering checks every element. takeWhile and dropWhile stop at a boundary. This is the most common interview trap.
fun main() {
val students = sampleStudents()
val numbers = listOf(2, 4, 1, 6, 8)
println("passed = " + students.filter { it.marks >= 80 }.map { it.name })
println("not passed = " + students.filterNot { it.marks >= 80 }.map { it.name })
println("indexed = " + students.filterIndexed { index, _ -> index % 2 == 0 }.map { it.name })
println("partition = " + students.partition { it.marks >= 80 })
println("takeWhile = " + numbers.takeWhile { it % 2 == 0 })
println("dropWhile = " + numbers.dropWhile { it % 2 == 0 })
println("takeLastWhile = " + numbers.takeLastWhile { it % 2 == 0 })
println("dropLastWhile = " + numbers.dropLastWhile { it % 2 == 0 })
}
Use filter when matching elements can appear anywhere. Use takeWhile or dropWhile only when the list is already ordered around a boundary, such as "new unread notifications from the top".
Transforming
map keeps the same number of rows. mapNotNull may reduce rows. flatMap turns one row into many rows and then flattens the result.
fun main() {
val students = sampleStudents()
println("names = " + students.map { it.name })
println("numbered = " + students.mapIndexed { index, student -> "$index-${student.name}" })
println("valid numbers = " + listOf("10", "bad", "20").mapNotNull { it.toIntOrNull() })
println("indexed valid = " + listOf("10", "bad", "20").mapIndexedNotNull { index, value ->
value.toIntOrNull()?.let { index to it }
})
println("all courses = " + students.flatMap { it.courses })
println("unique courses = " + students.flatMap { it.courses }.distinct())
}
Android example: API models often contain nullable fields. mapNotNull is acceptable when dropping incomplete rows is a deliberate UI rule. If incomplete rows are server bugs, collect validation errors instead.
Accessing and searching
The OrNull versions are for expected absence. Throwing versions are for invariants.
fun main() {
val students = sampleStudents()
val empty = emptyList<Student>()
println("first = " + students.first().name)
println("firstOrNull = " + empty.firstOrNull())
println("last = " + students.last().name)
println("singleOrNull one = " + listOf("only").singleOrNull())
println("singleOrNull many = " + listOf("one", "two").singleOrNull())
println("list[1] = " + students[1].name)
println("get(1) = " + students.get(1).name)
println("getOrNull(10) = " + students.getOrNull(10))
println("elementAtOrElse = " + students.elementAtOrElse(10) { Student("Fallback", 0, emptyList(), 0) }.name)
println("find = " + students.find { it.name == "Priya" }?.name)
println("randomOrNull = " + empty.randomOrNull())
}
Prefer list[index] for normal list access. Prefer getOrNull(index) when an index comes from UI state and may be stale. Prefer single() only when exactly one result is a real business rule.
Checking and aggregation
any, all, and none short-circuit. fold is safer than reduce when empty input is possible.
fun main() {
val students = sampleStudents()
val emptyMarks = emptyList<Int>()
println("any high = " + students.any { it.marks > 85 })
println("all passed = " + students.all { it.marks >= 40 })
println("none failed = " + students.none { it.marks < 40 })
println("count passed = " + students.count { it.marks >= 80 })
println("sumOf marks = " + students.sumOf { it.marks })
println("average = " + students.map { it.marks }.average())
println("min age = " + students.minByOrNull { it.age }?.name)
println("max marks = " + students.maxByOrNull { it.marks }?.name)
println("fold empty = " + emptyMarks.fold(0) { total, mark -> total + mark })
println("reduce empty failure = " + runCatching { emptyMarks.reduce { total, mark -> total + mark } }.exceptionOrNull()!!::class.simpleName)
}
Common trap: emptyList<Int>().all { it > 0 } returns true. If at least one item must exist, check items.isNotEmpty() && items.all { ... }.
Sorting, grouping, and map creation
groupBy keeps all values per key. associateBy keeps one value per key and later duplicates replace earlier values.
fun main() {
val students = sampleStudents()
println("sorted age = " + students.sortedBy { it.age }.map { it.name })
println("sorted marks desc = " + students.sortedByDescending { it.marks }.map { it.name })
println("custom sort = " + students.sortedWith(compareBy<Student> { it.age }.thenBy { it.name }).map { it.name })
println("groupBy age = " + students.groupBy { it.age })
println("course counts = " + students.flatMap { it.courses }.groupingBy { it }.eachCount())
println("associateBy name = " + students.associateBy { it.name })
println("associate name to marks = " + students.associate { it.name to it.marks })
println("associateWith marks = " + students.associateWith { it.marks })
}
Android example: use associateBy(User::id) for fast lookup by stable ID, but validate duplicates if duplicate IDs indicate corrupt API data.
Combining, chunking, and running results
zip combines by position and stops at the shorter collection. chunked creates non-overlapping groups. windowed creates overlapping groups.
fun main() {
val names = listOf("Arun", "Priya", "Rahul")
val marks = listOf(80, 90)
val numbers = listOf(1, 2, 3, 4, 5)
println("zip = " + names.zip(marks))
println("unzip = " + listOf("Arun" to 80, "Priya" to 90).unzip())
println("zipWithNext = " + numbers.zipWithNext())
println("plus = " + (listOf(1, 2) + 3))
println("minus = " + (listOf(1, 2, 3, 2) - 2))
println("chunked = " + numbers.chunked(2))
println("windowed = " + numbers.windowed(3))
println("runningFold = " + numbers.runningFold(0) { total, n -> total + n })
println("runningReduce = " + numbers.runningReduce { total, n -> total + n })
}
Use chunked(100) for API batch limits. Use windowed(3) for moving averages or comparing neighboring values. Use runningFold when the UI needs every intermediate state, not just the final total.
Interview priority
Master these first: filter, map, flatMap, distinct, sortedBy, find, firstOrNull, getOrNull, any, all, none, count, sumOf, minByOrNull, maxByOrNull, groupBy, associateBy, partition, zip, chunked, windowed, fold, and reduce.
Interview questions
- Why is
filterdifferent fromtakeWhile? - Why can
associateBybe dangerous with duplicate keys? - Why does
reducefail on empty input butfolddoes not? - Why does
zipstop at the shorter list? - When should you use
single()instead offirst()? - Why should stale UI indexes usually use
getOrNull()? - How would you batch 10,000 pending IDs into API calls of 100 IDs each?
- How would you calculate a moving average with
windowed()?