Map #
Map is a collection of key-value pairs where each key is unique. It answers the question “what value is connected to this key?” in constant time O(1) — far more efficient than searching a list. Like List, Kotlin distinguishes Map (read-only) from MutableMap (modifiable) at the type level. Behind the scenes, Kotlin uses JVM implementations like HashMap, LinkedHashMap, or TreeMap, each with different characteristics. This article covers Map thoroughly: from creation and basic access, the available implementations, functional operations, to idiomatic usage patterns.
Creating a Map #
// mapOf() — immutable, pairs written with the infix 'to'
val capitals = mapOf(
"Indonesia" to "Jakarta",
"Japan" to "Tokyo",
"France" to "Paris",
"Brazil" to "Brasilia"
)
// emptyMap() — empty map
val empty = emptyMap<String, Int>()
// mutableMapOf() — modifiable
val scores = mutableMapOf(
"Budi" to 85,
"Sari" to 92,
"Ahmad" to 78
)
// buildMap — DSL builder for more flexible construction
val config = buildMap {
put("host", "localhost")
put("port", "5432")
put("database", "myapp")
if (System.getenv("DEBUG") == "true") {
put("logLevel", "DEBUG")
}
}
Key-Value Pair Types #
to is an infix function that creates a Pair<K, V>. You can also create a map from a list of pairs:
val pairs = listOf(
"one" to 1,
"two" to 2,
"three" to 3
)
val fromPairs = pairs.toMap()
// Or from two separate lists
val keys = listOf("a", "b", "c")
val values = listOf(1, 2, 3)
val fromZip = keys.zip(values).toMap()
println(fromZip) // {a=1, b=2, c=3}
Map Implementations: Choosing the Right One #
On the JVM, there are three main Map implementations. Kotlin by default uses LinkedHashMap (via mapOf and mutableMapOf) which preserves insertion order.
| Implementation | Order | Performance | When to Use |
|---|---|---|---|
HashMap | Not guaranteed | O(1) average | Maximum performance, order doesn’t matter |
LinkedHashMap | Insertion order (default) | O(1) average | When insertion order must be preserved |
TreeMap | Natural key order | O(log n) | When you need iteration in sorted order |
// HashMap — performance, order not guaranteed
val hashMap = HashMap<String, Int>()
hashMap["b"] = 2
hashMap["a"] = 1
hashMap["c"] = 3
println(hashMap) // unpredictable order
// LinkedHashMap — preserve insertion order (mapOf's default)
val linkedMap = LinkedHashMap<String, Int>()
linkedMap["b"] = 2
linkedMap["a"] = 1
linkedMap["c"] = 3
println(linkedMap) // {b=2, a=1, c=3} — insertion order
// TreeMap — natural key order (alphabetical for String)
val treeMap = java.util.TreeMap<String, Int>()
treeMap["b"] = 2
treeMap["a"] = 1
treeMap["c"] = 3
println(treeMap) // {a=1, b=2, c=3} — alphabetical order
// sortedMapOf — shortcut for TreeMap
val sorted = sortedMapOf("banana" to 2, "apple" to 1, "cherry" to 3)
println(sorted) // {apple=1, banana=2, cherry=3}
Accessing Elements #
val population = mapOf(
"Jakarta" to 10_500_000,
"Surabaya" to 2_800_000,
"Bandung" to 2_400_000,
"Medan" to 2_200_000
)
// Access with the [] operator — returns null if the key doesn't exist
println(population["Jakarta"]) // 10500000
println(population["Bali"]) // null
// get() — identical to []
println(population.get("Bandung")) // 2400000
// getOrDefault — return a default value if the key doesn't exist
println(population.getOrDefault("Bali", 0)) // 0
// getOrElse — compute the default value lazily
println(population.getOrElse("Bali") {
println("Computing default population...")
500_000
})
// getValue — throws NoSuchElementException if the key doesn't exist
println(population.getValue("Medan")) // 2200000
// population.getValue("Bali") // ✗ NoSuchElementException
// getOrPut (MutableMap only) — get the value or insert a default
val cache = mutableMapOf<String, Int>()
val computed = cache.getOrPut("fibonacci-10") {
println("Computing fibonacci...")
55 // computed only once, stored in the cache
}
println(cache.getOrPut("fibonacci-10") { 99 }) // 55 — from cache, 99 isn't computed
Map Structure Information #
val data = mapOf("a" to 1, "b" to 2, "c" to 3)
println(data.size) // 3
println(data.isEmpty()) // false
println(data.isNotEmpty()) // true
// Access keys, values, entries
println(data.keys) // [a, b, c]
println(data.values) // [1, 2, 3]
println(data.entries) // [a=1, b=2, c=3]
// Check existence
println("a" in data) // true (checks the key)
println(data.containsKey("z")) // false
println(data.containsValue(2)) // true
Modifying a MutableMap #
val inventory = mutableMapOf(
"Laptop" to 10,
"Mouse" to 50,
"Keyboard" to 25
)
// Add or replace
inventory["Monitor"] = 8 // add a new key
inventory["Laptop"] = 12 // replace an existing value
inventory.put("Headset", 15) // identical to []
// Add many at once
inventory.putAll(mapOf("Webcam" to 20, "Speaker" to 7))
// Remove
inventory.remove("Mouse") // remove by key
inventory.remove("Monitor", 8) // remove only if key-value matches
inventory.entries.removeIf { it.value < 10 } // remove based on a condition
// Update an existing value
inventory["Laptop"] = (inventory["Laptop"] ?: 0) + 5 // add 5 to the old value
// merge — combine with custom logic
inventory.merge("Keyboard", 10) { old, addition -> old + addition }
// if "Keyboard" exists: new value = old + addition
// if it doesn't: add it with the value 10
// compute — recompute the value for a key
inventory.compute("Laptop") { _, oldStock ->
if (oldStock == null) 1 else oldStock + 1
}
println(inventory)
Iteration #
val prices = mapOf("Coffee" to 15_000, "Tea" to 10_000, "Juice" to 20_000)
// Destructuring in a for loop — the most common way
for ((menu, price) in prices) {
println("$menu: Rp${\"%,d\".format(price)}")
}
// forEach with a lambda
prices.forEach { (menu, price) ->
println("$menu → Rp${\"%,d\".format(price)}")
}
// Iterate keys or values only
prices.keys.forEach { println(it) }
prices.values.forEach { println(it) }
// Iterate entries (full access to Map.Entry)
prices.entries.forEach { entry ->
println("${entry.key}: ${entry.value}")
}
// forEachIndexed doesn't exist directly on Map,
// but can be done via entries.forEachIndexed
prices.entries.forEachIndexed { i, (menu, price) ->
println("$i. $menu: Rp${\"%,d\".format(price)}")
}
Functional Transformations #
All operations below produce a new map or collection — the original map isn’t modified.
val grades = mapOf("Math" to 85, "Physics" to 72, "Chemistry" to 90, "Biology" to 68)
// filter — filter by entry (both key and value are available)
val passed = grades.filter { (_, v) -> v >= 75 }
println(passed) // {Math=85, Chemistry=90}
// filterKeys — filter by key only
val hardSciences = grades.filterKeys { it in listOf("Physics", "Chemistry") }
println(hardSciences) // {Physics=72, Chemistry=90}
// filterValues — filter by value only
val highGrades = grades.filterValues { it >= 80 }
println(highGrades) // {Math=85, Chemistry=90}
// mapValues — transform all values
val normalized = grades.mapValues { (_, v) -> v / 100.0 }
println(normalized) // {Math=0.85, Physics=0.72, ...}
// mapKeys — transform all keys
val abbreviations = grades.mapKeys { (k, _) -> k.take(3).uppercase() }
println(abbreviations) // {MAT=85, PHY=72, CHE=90, BIO=68}
// map — transform entries into a list (produces a List, not a Map)
val descriptions = grades.map { (subject, n) ->
"$subject: $n (${if (n >= 75) "Passed" else "Remedial"})"
}
descriptions.forEach { println(it) }
// any, all, none, count on Map
println(grades.any { (_, v) -> v >= 90 }) // true
println(grades.all { (_, v) -> v >= 60 }) // true
println(grades.count { (_, v) -> v >= 75 }) // 2
println(grades.none { (_, v) -> v > 100 }) // true
// Value aggregation
println(grades.values.average()) // 78.75
println(grades.values.sum()) // 315
println(grades.maxByOrNull { it.value }) // Chemistry=90
println(grades.minByOrNull { it.value }) // Biology=68
Merging Maps #
val mapA = mapOf("a" to 1, "b" to 2, "c" to 3)
val mapB = mapOf("b" to 20, "c" to 30, "d" to 40)
// + operator — merge, mapB's keys override mapA's on conflict
val merged = mapA + mapB
println(merged) // {a=1, b=20, c=30, d=40}
// - operator — remove keys
val subtracted = mapA - "b"
println(subtracted) // {a=1, c=3}
val subtractedSeveral = mapA - setOf("a", "c")
println(subtractedSeveral) // {b=2}
// Merge with custom logic (resolve conflicts manually)
fun <K, V> mergeWithLogic(
mapA: Map<K, V>,
mapB: Map<K, V>,
resolve: (K, V, V) -> V
): Map<K, V> {
val result = mapA.toMutableMap()
mapB.forEach { (key, valueB) ->
result.merge(key, valueB) { valueA, b -> resolve(key, valueA, b) }
}
return result
}
// Example: merge stock, take the largest value on conflict
val warehouseAStock = mapOf("Laptop" to 5, "Mouse" to 30, "Keyboard" to 10)
val warehouseBStock = mapOf("Laptop" to 8, "Monitor" to 15, "Keyboard" to 7)
val totalStock = mergeWithLogic(warehouseAStock, warehouseBStock) { _, a, b -> a + b }
println(totalStock) // {Laptop=13, Mouse=30, Keyboard=17, Monitor=15}
Conversion from Other Collections #
data class Student(val nim: String, val name: String, val gpa: Double)
val studentList = listOf(
Student("2021001", "Budi Santoso", 3.85),
Student("2021002", "Sari Dewi", 3.92),
Student("2021003", "Ahmad Fauzi", 3.71)
)
// associateBy — create a Map with keys from an element transformation
val mapByNim = studentList.associateBy { it.nim }
println(mapByNim["2021001"]?.name) // Budi Santoso
// associateWith — create a Map with values from an element transformation
val mapNameToGpa = studentList.associateWith { it.gpa }
// Map<Student, Double> — the Student object as the key
// associate — freely choose key and value
val nimToName = studentList.associate { it.nim to it.name }
println(nimToName) // {2021001=Budi Santoso, 2021002=Sari Dewi, ...}
// groupBy — group into a Map<K, List<V>>
val studentsPerBatch = studentList.groupBy { it.nim.take(4) }
studentsPerBatch.forEach { (batch, list) ->
println("Batch $batch: ${list.map { it.name }}")
}
// toMap from a list of Pairs
val pairs = listOf("x" to 10, "y" to 20, "z" to 30)
val mapFromPairs = pairs.toMap()
println(mapFromPairs) // {x=10, y=20, z=30}
Common Usage Patterns #
Cache / Memo Table #
// Memoization — store the results of expensive computations
val cache = mutableMapOf<Int, Long>()
fun fibonacci(n: Int): Long {
if (n <= 1) return n.toLong()
return cache.getOrPut(n) {
fibonacci(n - 1) + fibonacci(n - 2)
}
}
println(fibonacci(50)) // 12586269025 — fast because it's cached
Frequency / Histogram #
val text = "hello world kotlin is awesome kotlin is great"
val words = text.split(" ")
// Count the frequency of each word
val frequency = words.groupingBy { it }.eachCount()
println(frequency)
// {hello=1, world=1, kotlin=2, is=2, awesome=1, great=1}
// Or with fold
val manualFrequency = words.fold(mutableMapOf<String, Int>()) { acc, w ->
acc.apply { merge(w, 1, Int::plus) }
}
// Sort by highest frequency
val sorted = frequency.entries
.sortedByDescending { it.value }
.take(3)
.joinToString { "${it.key}=${it.value}" }
println("Top 3: $sorted") // Top 3: kotlin=2, is=2, hello=1
Configuration with Defaults #
// Map as configurable defaults that can be overridden
val defaultConfig = mapOf(
"host" to "localhost",
"port" to "5432",
"poolSize" to "10",
"timeout" to "30000"
)
val customConfig = mapOf(
"host" to "db.production.com",
"poolSize" to "50"
)
// Merge: defaults overridden by custom values
val finalConfig = defaultConfig + customConfig
println(finalConfig)
// {host=db.production.com, port=5432, poolSize=50, timeout=30000}
Reverse Index #
val nikToName = mapOf(
"3201234567890001" to "Budi Santoso",
"3201234567890002" to "Sari Dewi",
"3201234567890003" to "Ahmad Fauzi"
)
// Reverse the map: name becomes the key, NIK becomes the value
val nameToNik = nikToName.entries.associate { (nik, name) -> name to nik }
println(nameToNik["Sari Dewi"]) // 3201234567890002
Summary #
mapOfas the default — always start with an immutable map. Switch tomutableMapOfonly if you actually need modification. LikeList, expose asMapoutside the class, store asMutableMapinternally.- Safe access with
getOrDefaultorgetOrElse— avoidgetValueunless you’re sure the key exists.map[key]returns null for a missing key — handle it with Elvis?: 0orgetOrDefault.getOrPutfor the cache pattern — get an existing value or insert and return a new one. This is a very common and clean memoization pattern.LinkedHashMapis the default —mapOfandmutableMapOfuseLinkedHashMapwhich preserves insertion order. UsesortedMapOffor natural key order, or explicitHashMapif order truly doesn’t matter.filterKeys,filterValues,mapKeys,mapValues— use the right variant for the dimension you’re operating on. More expressive than genericfilterwhich needs destructuring every time.- The
+operator for merging —mapA + mapBproduces a new map where matching keys inmapBoverridemapA. Useful for the default-config + override pattern.associateByfor indexing — turn a list into a map withassociateBy { it.id }for O(1) access by identifier. Far more efficient thanlist.find { it.id == targetId }which is O(n).groupByfor aggregation — produce aMap<K, List<V>>for grouping data. Combine withmapValues { it.value.sumOf {...} }for per-group aggregation.