1. A Map is not iterated like a list: keys instead of indexes
If a List is like a numbered to-do list, then a Map is more like a phone book: by a “name” (key) we look up a “number” (value). That’s why trying to “walk through a map by indexes” sounds just as strange as asking “give me the third contact”, without specifying the order. In Kotlin, Map<K, V> stores “key → value” pairs, keys are unique, and the iteration order should not be the foundation of your logic.
The most important idea: Map is not a “list of pairs”, but a structure where data access goes through a key. And it’s exactly from this idea that the correct ways to iterate follow.
Reminder: how a Map is created and what Pair is
Before iterating, you need something to iterate over. Usually we create a map via mapOf(...) or mutableMapOf(...), passing in Pair<K, V> values. Most often, pairs are created via the infix function to, which looks like a little arrow “key to value”.
For example:
fun main() {
val prices = mapOf(
"coffee" to 250,
"tea" to 120
)
println(prices) // {coffee=250, tea=120}
}
This is just a starting point. Next, we’ll iterate over such maps in different ways—depending on the task.
Nuance: map[key] returns V?
When you read a value by key, you’re basically asking the map: “do you have such a key?”. If the key is missing, the map cannot “guess” a value, so it returns null. That’s why the expression map[key] has type V?, even if V itself is not nullable.
In practice it looks like this:
fun main() {
val ages = mapOf("Ann" to 20, "Bob" to 31)
val annAge = ages["Ann"]
val kateAge = ages["Kate"]
println(annAge) // 20
println(kateAge) // null
}
And this is where many beginners fall into a trap: they start iterating keys, and then pull the value via map[key], and suddenly run into null. In this lecture, we’ll go over iteration approaches that make such code simpler, safer, and more readable.
2. Map views: keys, values, entries
When you iterate a Map, you almost always want one of three things: iterate keys, iterate values, or iterate whole pairs. Kotlin makes this explicit: a map has keys, values and entries. These are not three different maps—they’re “windows” (views) into the same structure, just with a different perspective on the data.
Let’s lock this in with a small table—it will help every time your hand reaches to “iterate a Map like a List”.
| What you need to do | What to iterate | What you get in the loop |
|---|---|---|
| Only keys are needed | |
|
| Only values are needed | |
|
| Both key and value are needed | |
|
| Both key and value are needed (short syntax) | |
|
Iterating keys: for (k in map.keys)
Sometimes your logic doesn’t care about the value at all; the important thing is the set of keys. For example, you want to print the list of categories in our training console app (a hypothetical “expense tracker”) or check whether the category "food" exists at all.
fun main() {
val totalsByCategory = mapOf(
"food" to 1200,
"transport" to 450
)
for (category in totalsByCategory.keys) {
println(category)
}
// food
// transport
}
The advantage of this approach is that it honestly tells the reader: “I’m working with keys.” The downside: if you later still need values, you’ll either add a second pass, or start pulling map[category], and the V? topic comes back.
Iterating values: for (v in map.values)
If you only care about values, you can iterate values. For example, you want to compute the total sum across all categories (for now, without advanced collection operations—just a plain loop).
fun main() {
val totalsByCategory = mapOf(
"food" to 1200,
"transport" to 450
)
var sum = 0
for (value in totalsByCategory.values) {
sum += value
}
println("Total = $sum") // Total = 1650
}
This approach is great when keys truly aren’t needed. And yes, this happens more often than it seems: keys are often needed “for pretty output”, but not for computations.
Iterating pairs: for (entry in map.entries)
If you need both the key and the value, the most straightforward route is to iterate entries. Each element of entries is a Map.Entry<K, V>, i.e., a pair object with the fields key and value.
fun main() {
val totalsByCategory = mapOf(
"food" to 1200,
"transport" to 450
)
for (entry in totalsByCategory.entries) {
println("${entry.key} -> ${entry.value}")
}
// food -> 1200
// transport -> 450
}
This option usually beats “iterating keys + map[key]”, because you don’t do a repeated lookup by key and you don’t force yourself to deal with V?.
What is Map.Entry<K, V> and how is it different from Pair
Map.Entry is a “wrapper” for a single pair inside a Map. It’s important to understand: Entry is not necessarily a Pair. It’s a separate type that reflects the meaning of “a map element”. It provides access to entry.key and entry.value, and most importantly, it allows Kotlin and the standard library to build predictable APIs around Map.
To reinforce this, here’s a small “what consists of what” diagram:
flowchart TD
M["Map⟨K, V⟩"] --> KEYS["keys: Set⟨K⟩"]
M --> VALUES["values: Collection⟨V⟩"]
M --> ENTRIES["entries: Set⟨Entry⟨K, V⟩⟩"]
ENTRIES --> E["Entry⟨K, V⟩"]
E --> K["key: K"]
E --> V["value: V"]
Practical takeaway: when you iterate entries, you almost never need to reach back into the map by key. You’re already standing on the pair, holding it with both hands, and you can work directly.
3. Destructuring and choosing an iteration style
Destructuring: for ((k, v) in map)
Sometimes the notation entry.key / entry.value feels a bit verbose. Kotlin allows you to “unpack” the pair into two variables right in the for. This is called destructuring, and for a Map it looks very natural: for ((k, v) in map).
Important: when you write for ((k, v) in map), you are iterating pairs—just with more compact syntax.
fun main() {
val totalsByCategory = mapOf(
"food" to 1200,
"transport" to 450
)
for ((category, total) in totalsByCategory) {
println("$category: $total")
}
// food: 1200
// transport: 450
}
This option is usually the most readable if you need both parts of the pair and you don’t do anything overly complex inside the loop. Plus, it nudges you toward giving variables good names: category, total, name, age.
How to choose an iteration style
A rule that saves a lot of nerves: if you need only one part of the data, iterate only that (keys or values). If you need both—iterate pairs (entries or destructuring). This makes the code straightforward and makes mistakes less likely.
Let’s compare two approaches on the same example—print category -> total.
Approach “by keys, and I’ll fetch the value later”:
fun main() {
val totalsByCategory = mapOf("food" to 1200, "transport" to 450)
for (category in totalsByCategory.keys) {
val total = totalsByCategory[category]
println("$category -> $total")
}
// food -> 1200
// transport -> 450
}
It works, but here total has type Int?, which is mildly annoying.
Approach “by pairs”:
fun main() {
val totalsByCategory = mapOf("food" to 1200, "transport" to 450)
for ((category, total) in totalsByCategory) {
println("$category -> $total")
}
// food -> 1200
// transport -> 450
}
Here total is a regular Int, which is exactly what we want in 99% of cases.
4. Practice: report by categories
Let’s add this topic to the same app we’ve been building throughout the course: a console expense tracker. We’re not using OOP yet, so expenses can be stored as a simple Triple<String, String, Int>: (category, title, amount). Yes, it’s not ideal, but right now our task is to practice collections and iteration, not domain model design.
Below is a small piece of logic: we already have a list of expenses and want to build a Map “category → sum”, and then print a nice report. We won’t use advanced collection operations (that will come later); we’ll do everything with an honest loop.
fun buildTotalsByCategory(expenses: List<Triple<String, String, Int>>): Map<String, Int> {
val totals = mutableMapOf<String, Int>()
for (e in expenses) {
val category = e.first
val amount = e.third
val oldTotal = totals[category] ?: 0
totals[category] = oldTotal + amount
}
return totals
}
Notice totals[category] ?: 0. We read from a Map, get an Int?, and if it’s null (the category doesn’t exist yet), we treat the old sum as zero. This is a classic pattern for Map, and it pairs well with the fact that key access returns V?.
Now—printing the report. And this is where iterating a Map via destructuring becomes very handy:
fun printTotalsReport(totalsByCategory: Map<String, Int>) {
println("Totals by category:")
for ((category, total) in totalsByCategory) {
println("- $category: $total")
}
}
Now let’s assemble this into a micro-scenario:
fun main() {
val expenses = mutableListOf(
Triple("food", "coffee", 250),
Triple("food", "sandwich", 350),
Triple("transport", "metro", 70)
)
val totals = buildTotalsByCategory(expenses)
printTotalsReport(totals)
// Totals by category:
// - food: 600
// - transport: 70
}
Nuance: Map iteration order should not become “part of the logic”
When printing a report, you might want categories to appear “in the order they were added”. In practice, it can look as if there is an order in a Map. And sometimes it really will be preserved, because the standard Kotlin MutableMap implementation is LinkedHashMap, which preserves insertion order during iteration. But another implementation (HashMap) makes no promises about order.
Practical rule: don’t make your program’s correctness depend on the iteration order of a Map. If order matters as part of the requirements, that’s a separate task and it must be solved explicitly—not by “well, it prints nicely for me”.
7. Common mistakes when iterating a Map
Mistake No. 1: trying to iterate a Map by indexes, “like a list”.
Sometimes your brain, out of habit, looks for map[0], map[1], map.indices. But a map has no indexes: it’s not about positions, it’s about “key → value” mappings. If you need a “row number” while printing a report, you can number lines with a separate counter, but the iteration itself still should go through keys/values/entries or destructuring.
Mistake No. 2: iterating keys, then fetching values via map[key] and getting annoyed by V?.
This isn’t “Kotlin being picky”, it’s an honest model: a key can be missing. If you need pairs—iterate pairs. for ((k, v) in map) or for (entry in map.entries) usually removes the problem completely, because inside the loop you already hold both the key and the value without a repeated lookup.
Mistake No. 3: writing for (k in map.keys) println(map[k]!!).
This is a dangerous habit. Yes, in this specific situation “the keys are from this same map”, so map[k] really is not null. But you get used to resolving null issues with !!, and that habit will backfire elsewhere—where keys come from the outside (for example, from user input). It’s better to choose an iteration style right away that doesn’t force you to prove the obvious to the compiler.
Mistake No. 4: iterating values, and then suddenly needing to “figure out which key a value belongs to”.
This happens when you start with “I only need sums”, and then you decide to nicely print “category: sum”. If keys are actually needed—then you should iterate pairs. values are useful, but only when you’re sure keys do not participate in the logic at all.
Mistake No. 5: building program meaning around Map iteration order.
Today everything prints “as expected”, tomorrow you change the map implementation or the data starts arriving in a different order—and the output “shifts”. A correct program should not depend on incidental ordering. If order matters, it must be designed separately, not hoped for with “well, it works for me right now”.
GO TO FULL VERSION