Iterating Over Collections
Iterating over a collection means visiting each element it holds, one at a time, so you can read it, transform it, or act on it. Almost every Kotlin program does this constantly — walking a list of users, summing a list of prices, printing the entries of a map. Kotlin gives you several different ways to do it, from the classic for loop to functional-style calls like forEach, and picking the right one for the situation makes your code clearer and, sometimes, faster. This lesson covers every mainstream way to iterate in Kotlin, how iteration actually works under the hood, and the mistakes that trip up nearly everyone at least once.
Overview / How it works
Kotlin’s for loop is not a special-cased language construct the way it is in some languages — it works on anything that satisfies the Iterable<T> contract. That contract requires exactly one method: iterator(), which returns an Iterator<T>. An Iterator in turn exposes two members: hasNext(): Boolean, which reports whether there is another element to visit, and next(): T, which returns the next element and advances an internal cursor. List, Set, arrays, ranges (created with .. or until), and even String (iterating its characters) all expose an iterator, so all of them work with for (x in ...) without any special-casing by the compiler.
Under the hood, the compiler desugars for (element in collection) { body } into something equivalent to:
val iterator = collection.iterator()
while (iterator.hasNext()) {
val element = iterator.next()
body
}
Knowing this desugaring explains a lot of iteration behavior: iterator() is called exactly once, up front, and the traversal state (“where am I in the collection”) lives entirely inside that one Iterator object, not in the loop itself.
Map is not directly Iterable<T> for a single element type, since it holds pairs. Instead you iterate its entries property (a Set<Map.Entry<K, V>>), or the keys or values properties individually. Kotlin’s standard library also adds component1()/component2() extension functions to Map.Entry, which is what lets you write for ((key, value) in map) and destructure each entry directly instead of writing entry.key and entry.value.
Alongside the for loop, Kotlin’s collection classes offer functional-style iteration through extension functions like forEach and forEachIndexed. These are declared inline, meaning the compiler pastes the lambda’s code directly into the call site at compile time — there is no extra function-call or lambda-object overhead compared to a hand-written loop. The trade-off is that a lambda passed to forEach is not a real loop as far as the compiler is concerned, so break and continue cannot be used inside it (more on that in Common Mistakes).
For very large pipelines that chain several operations (filter, map, take, and so on), consider asSequence(). Ordinary collection operations are eager: each call like .filter { ... } walks the whole collection and builds a brand-new intermediate list before the next operation runs. A Sequence<T> is lazy: elements flow through the whole chain one at a time, and no intermediate lists are allocated. For a handful of elements the difference is invisible; for large data or long chains it matters.
Syntax
The table below summarizes the main forms you will use to iterate a collection.
| Form | Use case |
|---|---|
for (element in collection) { ... } |
The default choice: simple, readable, supports break/continue/return. |
for ((index, value) in collection.withIndex()) { ... } |
When you need both the position and the value, without a manual counter. |
for ((key, value) in map) { ... } |
Iterating a Map, destructuring each entry directly. |
collection.forEach { element -> ... } |
Functional style, e.g. as the last step of a chain; no break/continue. |
collection.forEachIndexed { index, element -> ... } |
Functional style with an index, no manual counter. |
Manual iterator() + while (it.hasNext()) |
When you need to remove elements safely during traversal. |
// General forms for iterating a collection
for (element in collection) {
// use element
}
for ((index, value) in collection.withIndex()) {
// use index and value
}
for ((key, value) in map) {
// use key and value
}
collection.forEach { element ->
// use element
}
collection.forEachIndexed { index, element ->
// use index and element
}
val iter = collection.iterator()
while (iter.hasNext()) {
val element = iter.next()
// use element, or call iter.remove()
}
Examples
Example 1: A basic for-in loop
fun main() {
val fruits = listOf("Apple", "Banana", "Cherry")
for (fruit in fruits) {
println(fruit)
}
}
Output:
Apple
Banana
Cherry
This is the loop you will reach for most often. fruits is a List<String>, so fruits.iterator() is called once, and each pass through the loop pulls the next string out with next() until hasNext() reports false.
Example 2: Iterating with an index using withIndex()
fun main() {
val colors = listOf("Red", "Green", "Blue")
for ((index, color) in colors.withIndex()) {
println("$index: $color")
}
}
Output:
0: Red
1: Green
2: Blue
withIndex() wraps each element in an IndexedValue<T>, a small data class holding an index: Int and a value: T. Because it is a data class, it has component1() and component2() generated for it, which is exactly what makes the destructuring pattern (index, color) in the for header legal. This is preferable to maintaining your own counter variable (var i = 0, incrementing it by hand) — it is shorter and removes an entire class of off-by-one mistakes.
Example 3: Iterating a Map and accumulating a value
fun main() {
val prices = mapOf("Coffee" to 3.5, "Tea" to 2.5, "Juice" to 4.0)
for ((item, price) in prices) {
println("$item costs $$price")
}
var total = 0.0
prices.forEach { (_, price) -> total += price }
println("Total: $$total")
}
Output:
Coffee costs $3.5
Tea costs $2.5
Juice costs $4.0
Total: $10.0
mapOf preserves insertion order (it is backed by a LinkedHashMap), so the entries print in the order they were declared. The first loop destructures each Map.Entry into item and price directly in the for header. The second part shows forEach on a Map: the lambda receives one Map.Entry parameter, and (_, price) destructures it inline, using _ to explicitly discard the key we do not need. total is declared with var rather than val because, unlike the collections above, its value genuinely needs to change on every iteration as prices accumulate.
How it works step by step
Walking through Example 1 in detail: fruits.iterator() runs once, producing an Iterator<String> positioned just before the first element. The loop condition calls hasNext(), which returns true; next() then returns "Apple" and advances the cursor. The loop body runs, printing it. The loop returns to the condition, calls hasNext() again, gets "Banana", then "Cherry", and finally hasNext() returns false once the cursor passes the last element, ending the loop.
Ranges deserve a special note. 0..3 or 0 until items.size creates an IntRange, which implements Iterable<Int> but, crucially, is backed by a specialized IntIterator whose next() is really nextInt(): Int. This means iterating an IntRange does not box each integer into an Int object the way a generic Iterable<Int> would — it stays as a primitive JVM int the whole way through, which is why index-based loops over ranges are cheap.
Common Mistakes
Mistake 1: Mutating a list while iterating it
fun main() {
val numbers = mutableListOf(1, 2, 3, 4, 5)
for (n in numbers) {
if (n % 2 == 0) {
numbers.remove(n)
}
}
println(numbers)
}
This compiles fine, but it throws at runtime. Removing an element from the underlying list changes its internal modification counter; the for loop’s Iterator checks that counter on the next call to next(), sees it changed from underneath it, and throws ConcurrentModificationException before println(numbers) is ever reached. Use the iterator’s own remove() method instead, which keeps the counters in sync:
fun main() {
val numbers = mutableListOf(1, 2, 3, 4, 5)
val iterator = numbers.iterator()
while (iterator.hasNext()) {
val n = iterator.next()
if (n % 2 == 0) {
iterator.remove()
}
}
println(numbers)
}
Output:
[1, 3, 5]
Mistake 2: Trying to break or continue inside forEach
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
numbers.forEach {
if (it == 3) break
println(it)
}
}
This does not compile at all: break and continue only work inside an actual loop construct (for, while, do-while). forEach‘s lambda is just a function call as far as the compiler’s control-flow analysis is concerned, so break has nothing to jump out of. If you need early exit, use a real for loop instead:
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
for (n in numbers) {
if (n == 3) break
println(n)
}
}
Output:
1
2
(If you only want to skip the current element rather than stop entirely, return@forEach inside the lambda acts like continue — but for anything involving break, reach for a plain for loop.)
Mistake 3: Off-by-one with a hand-written range
fun main() {
val items = listOf("a", "b", "c")
for (i in 0..items.size) {
println(items[i])
}
}
This prints a, b, c, and then throws IndexOutOfBoundsException. 0..items.size is an inclusive range, so with a 3-element list it evaluates to 0..3 — but valid indices only run from 0 to 2. Use indices (or 0 until items.size), which is exactly the valid index range and cannot overshoot:
fun main() {
val items = listOf("a", "b", "c")
for (i in items.indices) {
println(items[i])
}
}
Output:
a
b
c
Best Practices
- Default to a plain
for (x in collection)loop — it is the most readable option and the only one that supportsbreak,continue, and a barereturnfrom the enclosing function. - Reach for
withIndex()orforEachIndexedinstead of maintaining a manual counter variable; it removes an entire class of off-by-one bugs. - Never structurally modify a
MutableList/MutableMap/MutableSetwhile iterating it with aforloop orforEach. Use the iterator’s ownremove(), or build a new collection withfilter/filterNot. - Prefer
collection.indicesover hand-rolled ranges like0..collection.sizewhen you need index-based access. - Use
forEach/forEachIndexedfor short, side-effect-only steps at the end of a functional chain; use a realforloop when you need early exit or complex branching. - Destructure
Mapentries directly withfor ((key, value) in map)instead of repeatedly writingentry.keyandentry.value. - For long pipelines of
filter/map/takeover large collections, considerasSequence()to avoid building intermediate lists at every step. - Remember that iterating a
valcollection is completely fine even thoughvalonly prevents reassigning the reference — iteration reads the current contents, it does not care whether the reference itself is reassignable.
Practice Exercises
1. Given val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), write a program that uses a for loop to print only the even numbers, one per line.
2. Given val names = listOf("Ana", "Bilal", "Chen"), use withIndex() to print each name prefixed by its 1-based position, e.g. 1. Ana.
3. Given val scores = mapOf("Ana" to 82, "Bilal" to 64, "Chen" to 91), iterate the map to print every student who scored 70 or above, then print the average of all scores (hint: accumulate a running total in a var while you iterate).
Summary
for (x in collection)works on anything implementingIterable<T>, which the compiler desugars into a call toiterator()followed by awhile (hasNext())loop callingnext().withIndex()gives you index and value together via a destructurableIndexedValue, avoiding manual counters.Mapis iterated through itsentries,keys, orvalues, andMap.Entrycan be destructured directly into(key, value).forEach/forEachIndexedare inlined functional alternatives to a loop, but their lambdas cannot usebreakorcontinue.- Never remove or add elements to a mutable collection while a
forloop orforEachis iterating it — use the iterator’s ownremove()instead, or build a new filtered collection. collection.indicesis safer than a hand-written0..collection.sizerange for index-based loops.asSequence()switches multi-step chains from eager (building intermediate collections at every step) to lazy, element-by-element evaluation.
