1. How for works under the hood
If you’re just starting to program, for (x in collection) can look like a magic phrase from a spellbook: say it—and the elements start landing in your hands by themselves. And it really is convenient. But as soon as you want to do something a bit more complex—for example, “iterate a collection in a special way,” “stop halfway,” “skip some elements,” or “explain a weird runtime error”—there will be too much magic, and it’ll start biting.
Good news: there’s no real magic there. In Kotlin, for relies on a simple idea: an object can be iterated if it can provide an iterator. An iterator is like a “remote control for iteration”: it knows whether there’s a next element and can return it.
Iterable and Iterator: the basic roles
When you see for (x in something) in code, Kotlin has to figure out: “OK, but how do I iterate over this something?” The mechanics are built on two entities:
- Iterable<T> is the contract “you can iterate over me.”
- Iterator<T> is the object that performs the iteration right now.
A helpful mental picture: the collection/source provides an iterator, and the iterator then “walks” through the elements.
flowchart LR
A["Iterable⟨T⟩
(data/source)"] -->|"iterator()"| B["Iterator⟨T⟩
(iteration process)"]
B -->|"next()"| C["T (next element)"]
Iterable<T>: “a promise that I can be iterated”
In the Kotlin standard library, there is the interface Iterable<T>. It’s not a “data container” and not a “list”; it’s specifically a contract: the object promises that you can move through it step by step, obtaining elements.
Formally, the contract is very simple: Iterable<T> must be able to return an Iterator<T> via the iterator() function.
To simplify the model:
- Iterable<T> is an “object that can be iterated.”
- Iterator<T> is a “device that performs the iteration right now.”
Iterator<T>: two steps—hasNext() and next()
An iterator stores the “current position” of the traversal and can move forward. It has two basic methods:
- hasNext(): answers the question “are there more elements?”
- next(): returns the next element and advances the position forward
It sounds dry, but in practice it’s like a queue at a coffee shop: while there are people (hasNext()), you can take the next one (next()), serve them—and the queue moves.
Mini example: get an iterator from a list and print the elements.
fun main() {
val names = listOf("Ann", "Bob", "Cate")
val it = names.iterator()
while (it.hasNext()) {
val name = it.next()
println(name) // Ann, then Bob, then Cate
}
}
Yes, this looks longer than for. And yes, that’s exactly why for exists.
An iterator is single-use: why you can’t “rewind”
When you watch a movie on streaming, you can rewind. When you use an iterator, it’s like watching a live broadcast: frames go by, and the iterator is not obligated to support going back. This is an important concept that often surprises beginners.
Let’s show it in code:
fun main() {
val nums = listOf(1, 2, 3)
val it = nums.iterator()
while (it.hasNext()) {
print(it.next())
}
println() // 123
// Second attempt with the same iterator:
println(it.hasNext()) // false
}
Notice: nums didn’t “disappear”; what disappeared is the ability of this specific iterator to keep iterating, because it already reached the end.
Collection vs iterator: who stores data and who stores “progress”
It’s easy to get confused here, so let’s lock in the model. A collection is the data. An iterator is the “iteration cursor.”
A small table (it’s genuinely worth rereading twice):
| Entity | What it is | What it stores | What it can do |
|---|---|---|---|
| Iterable<T> / collection | source of elements | the elements themselves | provide an iterator |
| Iterator<T> | iteration process | the current position | hasNext() and next() |
Quick comprehension check: if you need to repeat the traversal, you create a new iterator but use the same collection.
3. How for (x in collection) works
Key idea: for in Kotlin is not a separate “special mode.” It’s convenient syntax that performs the same traversal, just without the routine.
Let’s compare two versions (both are correct). The “human” version:
fun main() {
val names = listOf("Ann", "Bob", "Cate")
for (name in names) {
println(name)
// Ann
// Bob
// Cate
}
}
And the “mechanical,” but honest version:
fun main() {
val names = listOf("Ann", "Bob", "Cate")
val it = names.iterator()
while (it.hasNext()) {
val name = it.next()
println(name)
// Ann
// Bob
// Cate
}
}
Once you understand this equivalence, you’ll stop perceiving for as magic. It’s like autopilot: you don’t have to turn the steering wheel manually every time, but it’s useful to know the steering wheel exists.
“Equivalent of for” as a reading cheat sheet
Sometimes you read someone else’s code and see: “Whoa, there’s a manual iterator, while, hasNext()… what’s going on?” To avoid getting lost, keep this simple cheat sheet in mind:
for (x in collection) {
body(x)
}
mentally turns into:
val it = collection.iterator()
while (it.hasNext()) {
val x = it.next()
body(x)
}
This is not necessarily literally the same bytecode in all situations, but as a thinking model it works great.
4. Practice: SimplePlanner and printing tasks
Let’s keep evolving our console app (let’s call it SimplePlanner): it stores a list of task strings in a MutableList<String>. Today we won’t build the whole command interface, but we’ll add one useful function: print all tasks.
Printing tasks “manually,” via an iterator
Why through an iterator? Not because you always need to, but so you can see the mechanics and stop being afraid of it.
fun printTasksManual(tasks: List<String>) {
val it = tasks.iterator()
var number = 1
while (it.hasNext()) {
val task = it.next()
println("${number}. $task") // 1. ..., 2. ...
number++
}
}
Let’s test it in main:
fun main() {
val tasks = mutableListOf("Buy milk", "Read Kotlin docs", "Sleep")
printTasksManual(tasks)
// 1. Buy milk
// 2. Read Kotlin docs
// 3. Sleep
}
Yes, this is a bit longer than for ((i, task) in tasks.withIndex()). But right now our goal is to see the “engine,” not just the nice bodywork.
The same idea, but shorter: for as the default style
After you’ve seen the “engine,” you can calmly return to normal syntax. In most cases, for is the best option: it’s shorter, more readable, and gives you fewer chances to make a mistake (for example, forgetting hasNext() and calling next() into the void).
Task printing code will typically look like this:
fun printTasks(tasks: List<String>) {
var number = 1
for (task in tasks) {
println("${number}. $task") // 1. ..., 2. ...
number++
}
}
Same meaning, but easier to read.
5. When you need an explicit iterator and what to keep in mind
In your early programming days, you can go for years without a manual iterator and feel great. But sometimes manual control of iteration is useful: you want to control the steps, stop on a condition, or perform a special operation during traversal without turning a for loop into a mess.
Another common scenario is carefully modifying a mutable collection during iteration (but that’s a separate topic; here we’re only laying the foundation, without details).
How Kotlin knows an object can be used in for
Kotlin doesn’t guess—it looks for the “right shape.”
An object can be used in for (x in obj) if Kotlin can obtain an iterator from it. In practice, this boils down to a rule: the object must have an iterator() function, and the returned iterator must have the hasNext() and next() methods.
For today, it’s enough to remember:
- List, Set, Map (via entries, keys, values) and many other standard collections are already “iterable”—you can use them in for right away.
- If you create your own class, it won’t become “iterable” by itself until it supports iterator().
6. Typical mistakes when working with Iterable and Iterator
Mistake No. 1: confusing the collection and the iterator (“I took an iterator—where are my elements?”).
A collection is the storage of elements; an iterator is a temporary object that indicates “where we are right now” in the traversal. An iterator is not obligated to do everything a collection can: you don’t ask it for size, you don’t “rewind” it, and you don’t expect it to live forever. It often helps to say out loud: “collection is data, iterator is process.”
Mistake No. 2: calling next() without checking hasNext().
In manual iteration with a while loop, there’s an iron rule: first ask “is there a next one?”, then take the next one. If you call next() on an iterator that is already finished, you’ll get a runtime exception (i.e., the program will crash). for protects you from this mistake automatically, so the manual style requires more discipline.
Mistake No. 3: expecting you can reuse an iterator (“I’ll just iterate one more time with the same it”).
An iterator is single-use. If you reached the end, hasNext() will be false, and you won’t be able to “repeat” the traversal. You need to call collection.iterator() again and get a new iterator.
Mistake No. 4: treating for as “language magic” that can’t be explained.
for is syntactic comfort. It just hides the iterator boilerplate from you. Once you understand that for is based on iterator()/hasNext()/next(), the code becomes more predictable: you read other people’s solutions more easily, find the causes of errors faster, and transition more calmly to more complex iteration scenarios.
Mistake No. 5: writing a manual iterator where a regular for is needed.
A manual iterator is not “more professional.” It’s just a tool. If you don’t need special logic to control the traversal, then for will be shorter, safer, and clearer. In programming, there are many places where “shorter and clearer” is not laziness—it’s quality.
GO TO FULL VERSION