map, filter, and forEach
map, filter, and forEach are three of the most-used functions in Kotlin’s collections API. Together they let you transform, select, and act on the elements of a list without writing manual for loops full of index bookkeeping and temporary mutable variables. Once you understand how they work you will reach for them constantly, because they make everyday data-shuffling code shorter, safer, and easier to read at a glance.
Overview / How it works
map, filter, and forEach are not language keywords — they are ordinary extension functions defined on Iterable<T> (and, with the same names, on Sequence<T> and arrays) in the Kotlin standard library. Each one takes a lambda and applies it to every element of a collection, but they differ in what they do with the lambda’s result:
mapcalls the lambda on each element and collects the return values into a brand-newList. The output list is the same size as the input, but its element type can be completely different (for example,List<Person>in,List<String>out).filtercalls a lambda that returnsBoolean(a predicate) on each element and keeps only the elements for which it returnedtrue, again producing a newListof the same element type, possibly shorter.forEachcalls the lambda purely for its side effect (printing, logging, updating an external variable). Its lambda returnsUnit, andforEachitself returnsUnit— there is no new collection to capture.
A crucial point that trips up many newcomers: none of these functions mutate the collection they are called on. map and filter always return a new, read-only List; the original collection is left completely untouched, even if it was a MutableList. This follows Kotlin’s general preference for immutable data — you build new collections from old ones instead of editing in place.
Under the hood, map, filter, and forEach are declared with the inline modifier. That tells the compiler to paste the lambda’s bytecode directly at the call site instead of allocating a separate function object to hold it, so a chain like list.filter { ... }.map { ... } has essentially the same runtime cost as a hand-written loop — you get readability without paying a performance tax. The trade-off is that each element in the original collection is fully visited by filter before map even starts on the result; if you chain many operations over very large collections and want each element to flow through the whole pipeline one at a time, that’s what asSequence() and lazy Sequence operations are for — a topic covered in its own lesson.
Syntax
Simplified versions of the real standard-library declarations look like this:
// General form (simplified signatures from kotlin.collections)
inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R>
inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T>
inline fun <T> Iterable<T>.forEach(action: (T) -> Unit)
| Function | Purpose | Returns |
|---|---|---|
map |
Transform each element into something else | New List<R> |
filter |
Keep only elements matching a predicate | New List<T> |
forEach |
Perform a side effect for each element | Unit (nothing) |
mapNotNull |
Transform, dropping any null results | New non-null List<R> |
filterNot |
Keep elements that do NOT match a predicate | New List<T> |
Inside the lambda, when there is exactly one parameter you can refer to it implicitly as it instead of naming it, and because these are the last (and only) parameter, Kotlin lets you write the lambda outside the parentheses — that’s why you see list.map { it * 2 } instead of list.map({ it -> it * 2 }).
Examples
Example 1: map — transforming numbers
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
val squares = numbers.map { it * it }
println(squares)
}
Output:
[1, 4, 9, 16, 25]
map visits every element of numbers in order, squares it, and collects the five results into a new List<Int> called squares. The original numbers list is unchanged and still holds [1, 2, 3, 4, 5].
Example 2: filter — keeping only some elements
fun main() {
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val evens = numbers.filter { it % 2 == 0 }
println(evens)
}
Output:
[2, 4, 6, 8, 10]
The predicate { it % 2 == 0 } is evaluated for each of the ten numbers; only the ones where it returns true survive into the new list. Order is preserved, and the list can be shorter than the original — here it goes from ten elements down to five.
Example 3: forEach — acting on each element
fun main() {
val fruits = listOf("apple", "banana", "cherry")
fruits.forEach { fruit ->
println("I like $fruit")
}
}
Output:
I like apple
I like banana
I like cherry
Here the lambda parameter is named explicitly as fruit instead of using the implicit it, purely for readability. forEach produces no new collection — it exists only to run println once per element, in the original order.
Example 4: chaining all three together
data class Person(val name: String, val age: Int)
fun main() {
val people = listOf(
Person("Alice", 30),
Person("Bob", 15),
Person("Charlie", 25),
Person("Dana", 17)
)
val adultNames = people
.filter { it.age >= 18 }
.map { it.name.uppercase() }
adultNames.forEach { println(it) }
}
Output:
ALICE
CHARLIE
This is the realistic shape these functions take in day-to-day code: filter narrows the list of people down to adults (Alice and Charlie), map transforms the survivors into their upper-cased names, and forEach is used only at the very end, once, to print the final result. Person is a data class, so it automatically gets a readable toString(), structural equals()/hashCode(), and a copy() function — none of which this particular example needs, but which come for free the moment you model data with a data class instead of a plain class.
How it works step by step
For the chain in Example 4, execution proceeds like this:
- 1.
filteriteratespeoplefrom first to last, evaluatingit.age >= 18for each. It builds a brand-new list containing onlyPerson("Alice", 30)andPerson("Charlie", 25), in that order. - 2.
mapthen iterates that intermediate two-element list, callingit.name.uppercase()on eachPerson, producing a newList<String>containing"ALICE"and"CHARLIE". - 3.
forEachiterates that final list and callsprintlnonce per element, causing the two lines of output.
Because map and filter are eager (not lazy), each stage fully finishes before the next one starts — filter allocates its whole intermediate list before map ever runs. For small and medium collections this is irrelevant; for very large pipelines with many chained steps, converting to a Sequence first with asSequence() avoids building those intermediate lists.
Common Mistakes
Mistake 1: using map for side effects instead of forEach
val numbers = listOf(1, 2, 3)
val result = numbers.map { println(it) }
// result is a List<Unit>: [kotlin.Unit, kotlin.Unit, kotlin.Unit]
// the return value is discarded -- a list nobody needs still gets built in memory
This compiles and even prints the numbers, but it’s misleading: map signals to readers that you care about the transformed values, yet here the resulting List<Unit> is thrown away. It also wastes memory building a list of nothing. When you only want a side effect, reach for forEach instead:
val numbers = listOf(1, 2, 3)
numbers.forEach { println(it) }
Mistake 2: assuming filter/map keep the MutableList type
val numbers = mutableListOf(1, 2, 3, 4, 5)
val evens = numbers.filter { it % 2 == 0 }
evens.add(6) // does not compile: filter returns List<Int>, not MutableList<Int>
Even though numbers is a MutableList, filter always returns the read-only List interface. The compiler correctly refuses to call add on it, because List exposes no mutating methods. If you need a mutable result, convert explicitly:
val numbers = mutableListOf(1, 2, 3, 4, 5)
val evens = numbers.filter { it % 2 == 0 }.toMutableList()
evens.add(6)
println(evens)
Output:
[2, 4, 6]
Mistake 3: reaching for !! inside map instead of handling nulls
val names: List<String?> = listOf("Ann", null, "Bob")
val lengths = names.map { it!!.length }
println(lengths)
This compiles, because !! is legal on any nullable type — but it throws a NullPointerException at runtime the instant map reaches the null element, crashing the whole program over one bad entry. The non-nullable-by-default type system is Kotlin’s headline safety feature; using !! here throws that safety away instead of handling the null. Prefer mapNotNull, which safely evaluates a nullable expression per element and automatically drops any null results:
val names: List<String?> = listOf("Ann", null, "Bob")
val lengths = names.mapNotNull { it?.length }
println(lengths)
Output:
[3, 3]
The ?.length safe call returns null for the null element instead of crashing, and mapNotNull filters that null out of the final list rather than including it.
Best Practices
- Use
forEachonly for side effects (printing, logging, mutating something outside the lambda); usemaponly when you actually need the transformed list it returns. - Chain
filterbeforemapwhen you need both, so you transform only the elements you’re keeping instead of transforming everything and filtering afterward. - Prefer a regular
forloop overforEachwhen you need tobreakorcontinuepartway through —forEach‘s lambda cannot use non-localbreak/continue. - Reach for
mapNotNull,filterNotNull, or safe calls (?.,?:) instead of!!when a collection may contain nulls. - Remember
filterandmapalways return a newList, never mutate the receiver — if you need a mutable result, call.toMutableList()explicitly on the result. - For long chains over large collections, consider
asSequence()to avoid allocating an intermediate list at every step. - Give lambda parameters explicit names (like
fruit -> ...) instead ofitwhenever it meaningfully improves readability, especially in nested lambdas where a bareitwould be ambiguous.
Practice Exercises
- Exercise 1: Given
val words = listOf("kotlin", "is", "fun", "to", "learn"), usefilterto keep only the words with more than 2 characters, thenmapthem to their lengths. Expected output:[6, 3, 5]. - Exercise 2: Given
val scores = listOf(55, 90, 62, 78, 40, 99), usefilterto find the passing scores (>= 60), then useforEachto print each one on its own line prefixed with"Pass: ". - Exercise 3: Given a
data class Product(val name: String, val price: Double)and a list of several products, usefilterandmaptogether to produce aList<String>of the names of all products priced under 20.0, then print that list. Try rewriting yourmaplambda to useitversus a named parameter and see which reads more clearly for your case.
Summary
maptransforms every element into a new value and returns a newListof the results.filterkeeps only the elements matching aBooleanpredicate and returns a new, possibly shorterList.forEachperforms a side effect per element and returnsUnit— it never produces a new collection.- None of these functions mutate the original collection, even when called on a
MutableList—mapandfilteralways return the read-onlyListtype. - They are
inlinefunctions, so chaining them costs about the same as a hand-written loop. - Prefer
mapNotNulland safe calls over!!when working with nullable elements. - For very large collections or long chains,
asSequence()avoids building intermediate lists at each step.
