Kotlin maps are everywhere in Android development. Caches, config objects, API response handling – maps show up constantly, and how you use them matters more than most developers think.
This article covers everything from basic creation to thread-safe access in coroutine scopes. You’ll learn which map type to pick, how to avoid the pitfalls that trip up even experienced developers, and how to write transformation pipelines that actually stay readable.
Iterating Over Maps in Kotlin

There are four main ways to loop through a map. Each one fits a different situation, and the performance difference between them is small enough that readability should drive your choice.
Destructured for loop:
for ((key, value) in map) { ... }
This is what most Kotlin developers reach for first. Clean, readable, and the compiler inlines it well. You get direct access to both key and value without calling .get() separately.
forEach with lambda:
map.forEach { (key, value) -> ... }
Functionally identical to the for loop. The standard library’s forEach is inlined, so there’s no extra lambda allocation overhead on the JVM. But you can’t use break or continue inside it, which matters in certain control flow situations.
Iterating over keys, values, or entries independently:
map.keysgives you aSetof all keysmap.valuesgives you aCollectionof all valuesmap.entriesgives you aSet<Map.Entry>
Baeldung’s Kotlin guide notes that iterating over map.keys and calling map[key] to get each value is less efficient than iterating entries directly. You’re making separate lookup calls for keys you already could have accessed through the entry.
onEach for side effects in chains:
onEach { } runs an action on each entry and returns the original map. Useful when you want to log or debug mid-chain without breaking a transformation pipeline. Jetpack Compose’s internal code uses optimized fastForEach extensions on lists to skip iterator allocation in hot paths like onDraw, but for most application-level map iteration, the standard approaches work fine.
Merging and Combining Maps

Combining two maps seems straightforward until you hit duplicate keys. Kotlin gives you several options, and they differ in how conflicts are resolved.
| Method | Duplicate Key Behavior | Returns |
|---|---|---|
| map1 + map2 | Right side wins | New read-only map |
| putAll() | New values overwrite old | Modifies in place |
| getOrPut { } | Keeps existing value | Returns existing or new value |
| Java’s merge() | Custom resolution lambda | Modifies in place |
The + operator is the cleanest option for simple merges. If map1 has "a" to 1 and map2 has "a" to 2, the result will contain "a" to 2. Right side always takes priority.
For custom merge logic (like summing values for duplicate keys), you’ll need to loop manually or use Java’s merge() function, which is accessible from Kotlin on the JVM:
map.merge("key", newValue) { old, new -> old + new }
Forbes shares over 80% of business logic across iOS and Android using Kotlin Multiplatform, and map merging is exactly the kind of shared data-layer operation that benefits from a single implementation. When you’re maintaining config maps, feature flags, or API response caches in a cross-platform shared module, consistent merge behavior across platforms prevents subtle bugs.
JetBrains’ Developer Ecosystem surveys show Kotlin Multiplatform usage jumped from 7% in 2024 to 18% in 2025. A lot of that growth comes from teams sharing exactly these kinds of collection operations in common modules.
Kotlin Map Performance Characteristics

Picking the right map type is a performance decision. The difference between O(1) and O(log n) is nothing for 50 entries. It’s everything for 500,000.
| Operation | HashMap | LinkedHashMap | TreeMap |
|---|---|---|---|
| get(key) | O(1) | O(1) | O(log n) |
| put(key, value) | O(1) | O(1) | O(log n) |
| containsKey() | O(1) | O(1) | O(log n) |
| Iteration | O(n) | O(n), ordered | O(n), sorted |
| Memory overhead | Lower | Higher (linked list) | Higher (tree nodes) |
HashMap vs LinkedHashMap: Both offer O(1) lookups. But LinkedHashMap maintains a doubly-linked list of entries, which adds memory per entry. Romain Guy from Google’s Compose team built ScatterMap specifically because LinkedHashMap allocates a new object per entry, creating garbage collector pressure on Android.
For small datasets (under a few hundred entries), the difference is negligible. Array-based lookups or even a flat List of pairs can outperform a HashMap at that scale due to cache locality. This is why Android’s ArrayMap exists as an alternative for small collections.
JetBrains data from 2024 shows that 45% fewer null-reference bugs occur on projects using Kotlin’s strict null safety. This applies directly to map access patterns, where map[key] returns a nullable type and forces you to handle the missing-key case at compile time. Fewer NullPointerException crashes, less defensive coding.
If you’re doing software reliability work and profiling collection performance, check whether your bottleneck is actually in the map implementation or in what you’re doing inside the loop. Most of the time, it’s the latter.
Common Map Patterns and Pitfalls

Knowing the API is one thing. Knowing where developers actually get burned is another.
The getOrPut Caching Pattern
getOrPut() is Kotlin’s built-in memoization primitive. It checks if a key exists, and if not, computes a value, stores it, and returns it. All in one call.
val user = cache.getOrPut(userId) { fetchFromNetwork(userId) }
This pattern shows up constantly in real-world codebases for caching API responses, computed values, and configuration lookups. Kt. Academy’s “Effective Kotlin” recommends using maps for element lookup instead of lists, noting that HashMap lookup takes one comparison on average versus O(n) for a list scan.
But be aware: getOrPut is not thread-safe on a regular MutableMap. Two threads can both see the key as missing and both compute the value. For concurrent scenarios, use ConcurrentHashMap.computeIfAbsent() instead.
The map() Returns a List Surprise
This one trips up everyone at least once.
Calling .map { } on a Kotlin Map returns a List, not a new Map. The function transforms each entry into an element of a list. If you expected a map back, you’ll get a compile error or, worse, a logical bug that compiles fine but produces wrong results.
The fix: use mapKeys { } or mapValues { } when you want a Map output. Or chain .map { }.toMap() if you need to transform both key and value simultaneously into new pairs.
Mutable Map Aliasing Bugs
Pass a MutableMap to a function, and that function now holds a reference to your original map. Changes it makes show up everywhere.
This is a classic problem in any software development process that involves shared state. Kotlin’s answer is to default to read-only Map references whenever possible. If a function only needs to read the map, accept Map as the parameter type, not MutableMap.
McDonald’s and Netflix both use Kotlin Multiplatform in production, sharing core logic across platforms. Consistent immutability patterns in shared map operations prevent exactly these aliasing issues from appearing on one platform but not the other.
Null Keys and Null Values
HashMap and LinkedHashMap in Kotlin allow one null key and multiple null values. TreeMap does not allow null keys because it needs to compare them for sorting.
ConcurrentHashMap rejects both null keys and null values entirely.
If you’re writing unit tests for map-heavy code, test with null values explicitly. The behavior differences between map implementations can produce bugs that only surface when you switch from one type to another.
Maps and Kotlin Coroutines

Standard HashMap and MutableMap implementations are not thread-safe. Period. If multiple coroutines read and write to the same mutable map concurrently, you’ll get data corruption, lost updates, or ConcurrentModificationException crashes.
ConcurrentHashMap is the go-to solution. It uses segmented locking internally (fine-grained locks plus CAS operations since Java 8), so multiple threads can work on different segments simultaneously. Reads are mostly lock-free.
The Android Developers blog specifically recommends ConcurrentHashMap as a thread-safe collection for shared mutable state in coroutine-based code.
But there’s a catch. A blog post by “madhead” demonstrated that ConcurrentHashMap is thread-safe but not coroutine-safe. Its internal synchronized blocks can deadlock when combined with runBlocking inside coroutine scopes, because the coroutine might suspend while holding a lock that another coroutine on the same thread needs.
Kotlin-native alternatives for coroutine safety:
- Mutex from
kotlinx.coroutinesprotects map access with a suspend-aware lock that doesn’t block the thread - StateFlow or SharedFlow let you expose map state reactively, which fits well with Jetpack Compose’s observation model
- Confine all map mutations to a single coroutine dispatcher using
newSingleThreadContext()orDispatchers.Main
Kotlin’s official documentation on shared mutable state warns that volatile variables don’t solve concurrency problems either. Atomic reads and writes don’t make compound operations like “read, modify, write back” safe.
The JetBrains 2024 Developer Ecosystem survey found that Kotlin Flows, which pair well with reactive map state, are among the most used coroutine features. If your architecture already uses Flow for data streams, wrapping map state in a MutableStateFlow keeps everything consistent.
For teams working on custom app development with concurrent data access, the combination of ConcurrentHashMap for the backing store and Mutex for compound operations gives you both performance and correctness. Just don’t mix synchronized blocks with suspending functions inside them.
What Is a Map in Kotlin

A map is a collection of key-value pairs where every key is unique. Think of it as a dictionary. You give it a word (the key), and it hands back a definition (the value).
Kotlin splits maps into two categories at the type level: read-only Map and MutableMap. Java doesn’t make this distinction in its type system, so if you’re coming from that background, this is one of the first things that’ll trip you up.
The read-only Map interface gives you access to entries, keys, and values. But you can’t add, remove, or change anything. MutableMap extends it with write operations like put, remove, and bracket assignment.
Here’s the part that catches people off guard. When you call mapOf(), the default backing implementation is LinkedHashMap. Not HashMap. That means your insertion order is preserved during iteration, which is a detail most developers don’t realize until they depend on it (or get burned by assuming otherwise).
Key difference from Java: Kotlin enforces immutability by default at the type level. You can still cast a read-only map to a mutable one, but the compiler actively discourages it. This approach to null safety and collection immutability is a big reason why Kotlin has gained traction across software development teams.
Google estimates that 70% of the top 1,000 apps on the Play Store are written in Kotlin, including 60 of Google’s own apps like Maps and Drive (Dice, 2024). JetBrains reports that 95% of the top 1,000 Android apps include some Kotlin code.
The Map interface isn’t technically a subtype of Collection in Kotlin’s type hierarchy. It stands on its own. But it’s still considered a core collection type alongside List and Set, and the standard library treats it that way.
How to Create a Map in Kotlin

There are several ways to initialize maps. The right choice depends on whether you need the map to be mutable, what ordering guarantees matter, and how complex your initialization logic is.
Read-Only Map Initialization
The simplest approach. Call mapOf() with pairs using the to infix function:
val colors = mapOf("red" to "#FF0000", "blue" to "#0000FF")
This returns a read-only map backed by LinkedHashMap. You get insertion-order iteration out of the box.
For an empty map, you need to specify the types explicitly:
val empty = emptyMap<String, Int>()
One line. No fuss. The Kotlin standard library’s type inference handles the rest when you pass in pairs directly.
Mutable Map Initialization
mutableMapOf() creates a LinkedHashMap that you can modify after creation.
hashMapOf() gives you a HashMap with no ordering guarantee, which is slightly faster when you don’t care about iteration order.
sortedMapOf() returns a TreeMap where keys are sorted by natural ordering. Useful when you need range queries or alphabetical key traversal.
linkedMapOf() explicitly creates a LinkedHashMap. Functionally the same as mutableMapOf(), but more readable when you want to signal intent.
According to the Stack Overflow Developer Survey 2024, Kotlin ranked as the 4th most loved programming language with 58.2% developer satisfaction. That tracks with how clean the collection API feels compared to older JVM languages.
Building Maps from Existing Collections
The buildMap function, stable since Kotlin 1.6, is where things get interesting. It gives you a lambda with a MutableMap receiver, then returns an immutable map once the block finishes.
val config = buildMap { put("timeout", 30); put("retries", 3) }
This pattern works well when your map construction involves conditional logic or merging data from multiple sources. Took me a while to start using it consistently, but once I did, I stopped reaching for mutableMapOf plus .toMap() at the end.
You can also convert lists into maps with associate, associateBy, or associateWith. These extension functions on Iterable are cleaner than manually looping and inserting. If you’re working with Kotlin data classes, associateBy is particularly handy for indexing objects by a property like an ID field.
Map Types in Kotlin

Kotlin provides three main map implementations. Each one trades off between speed, memory, and ordering guarantees.
| Map Type | Ordering | Lookup Time | Best For |
|---|---|---|---|
| HashMap | None | O(1) average | General-purpose, performance-critical code |
| LinkedHashMap | Insertion order | O(1) average | When iteration order matters |
| TreeMap | Sorted by key | O(log n) | Range queries, sorted traversal |
HashMap is the raw speed option. No ordering, no extra memory for maintaining linked entries. If you’re building a cache or lookup table where you never iterate in a specific order, this is the pick.
Romain Guy, a Google engineer working on Jetpack Compose, created ScatterMap as an alternative to LinkedHashMap for the Compose toolkit. His benchmarks on a Pixel 6 showed that the standard LinkedHashMap creates a new object per entry, which pressures the garbage collector on Android devices.
LinkedHashMap is what mapOf() and mutableMapOf() give you by default. It maintains a doubly-linked list across all entries so iteration follows insertion order. The extra memory overhead is small, but it’s there. On Android specifically, where memory budgets are tighter, this can matter at scale.
TreeMap sorts keys using their natural ordering or a custom comparator. Access and insertion are O(log n), not O(1). You’d use it when you need operations like “give me all keys between X and Y.” It wraps Java’s TreeMap under the hood.
Netflix, Uber, and Pinterest all use Kotlin in production. For teams building Android applications, the choice between these map types directly affects software scalability at the collection level.
How to Access and Read Map Entries

Kotlin gives you multiple ways to get values out of a map. The right one depends on how you want to handle missing keys.
Bracket syntax is the most common. map["key"] returns the value or null if the key doesn’t exist. Simple, nullable, no exceptions.
getValue() throws a NoSuchElementException if the key is missing. Use it only when you’re certain the key exists. I’ve seen this blow up in production more than once when someone assumed a config value would always be present.
getOrDefault() returns a fallback value you specify. Clean and predictable:
val timeout = settings.getOrDefault("timeout", 30)
getOrElse() takes a lambda instead of a static default. Good for computed fallbacks or logging when a key is absent.
For iteration, destructuring declarations keep things readable:
for ((key, value) in userMap) { println("$key: $value") }
JetBrains’ 2024 Developer Ecosystem survey found that 75% of Kotlin users express satisfaction with the language. The collection access API is a big part of that. Compared to Java’s map.get() returning a raw nullable without compiler enforcement, Kotlin’s approach works with the type system instead of against it.
You can also access map.keys, map.values, and map.entries directly as separate collections. The entries property returns a set of Map.Entry objects, which is useful when you need both key and value without destructuring.
How to Add, Update, and Remove Map Entries
All mutation operations require a MutableMap. If you try to call put on a read-only Map reference, the compiler stops you before the code even runs.
Adding and Updating Entries
Two approaches that do the same thing:
map.put("key", value)returns the previous value (or null)map["key"] = valueis the idiomatic shorthand most Kotlin developers prefer
For bulk insertion, putAll() accepts another map and merges it in. The += operator does the same thing with cleaner syntax:
userMap += mapOf("name" to "Alice", "role" to "admin")
If you’re building mobile applications with Kotlin, you’ll use these patterns constantly for managing UI state, configuration, and cached API responses.
Removing Entries and Clearing Maps
remove(key) deletes the entry and returns its value. There’s also a conditional version: remove(key, value) only removes the entry if the current value matches what you pass in. Handy for compare-and-delete patterns.
The -= operator removes keys:
map -= "oldKey"
clear() wipes the entire map. After calling it, the map’s size is 0 but the underlying LinkedHashMap or HashMap instance stays allocated.
One thing that bites people working in larger teams: passing a MutableMap around and modifying it from different parts of the codebase leads to aliasing bugs. Two references to the same mutable map means changes in one place show up everywhere. If you’ve spent time debugging something like that, you already know why Kotlin defaults to immutability.
Filtering, Mapping, and Transforming Maps

Kotlin’s functional programming support makes map transformations compact and expressive. But there are gotchas here that catch even experienced developers.
Filtering Map Entries
filter { } takes a predicate on the full entry (key and value both) and returns a new map with only matching entries.
For more targeted filtering:
- filterKeys { } checks only keys
- filterValues { } checks only values
All three return a new Map, leaving the original untouched. This matters when you’re writing test-driven code and need predictable, side-effect-free transformations.
Transforming Keys and Values
Here’s the gotcha everyone hits at least once. Calling .map { } on a Kotlin Map returns a List, not a Map. Your mileage may vary on how quickly you’ll discover this.
To actually transform and get a map back:
mapKeys { }produces a new map with transformed keysmapValues { }produces a new map with transformed values
These are what you want 90% of the time. The map { } function is still useful when you need to convert map entries into a list of something else entirely, like UI elements or formatted strings.
The flatMap { } function flattens nested structures. If your map values are lists, and you need a single flat list, this is the tool.
Stack Overflow’s 2024 survey shows that job postings mentioning Kotlin grew by 30% year-over-year. A lot of that demand comes from teams needing developers who can write clean collection pipelines like these, especially for back-end development with Spring Boot and Ktor.
Converting Maps to Other Collection Types
Maps convert easily to other structures:
| Conversion | Method | Returns |
|---|---|---|
| Map to list of pairs | .toList() | List |
| Map to sorted map | .toSortedMap() | SortedMap |
| Map to mutable map | .toMutableMap() | MutableMap |
| List of pairs to map | .toMap() | Map |
Chaining these with Kotlin lambda functions is where the real power shows up. Something like users.filterValues { it.isActive }.mapKeys { it.key.lowercase() } reads almost like English, and that readability is precisely why teams migrating from Java tend to write fewer bugs with Kotlin collections.
FAQ on Maps In Kotlin
What is the difference between Map and MutableMap in Kotlin?
Map is a read-only interface. You can access keys, values, and entries but can’t modify anything after creation. MutableMap extends it with write operations like put, remove, and bracket assignment. Kotlin enforces this distinction at compile time.
How do you create an empty map in Kotlin?
Call emptyMap<String, Int>() for a read-only empty map. You need to specify generic types explicitly since the compiler can’t infer them without initial values. For a mutable version, use mutableMapOf<String, Int>() instead.
What is the default map implementation in Kotlin?
Both mapOf() and mutableMapOf() return a LinkedHashMap under the hood. This preserves insertion order during iteration. Most developers don’t realize this until they depend on ordering behavior or switch to hashMapOf().
How do you iterate over a map in Kotlin?
The most common approach uses destructuring: for ((key, value) in map). You can also use forEach with a lambda. Iterating over map.entries directly is more efficient than looping through map.keys and calling get() separately.
What does getOrPut do in a Kotlin map?
getOrPut() checks if a key exists. If yes, it returns the value. If not, it computes a value from the lambda, stores it, and returns it. This is Kotlin’s built-in pattern for memoization and caching in mutable maps.
Why does calling map() on a Kotlin Map return a List?
The .map { } function transforms each entry into a new element, producing a List. It doesn’t return a Map. Use mapKeys { } or mapValues { } to get a Map back, or chain .map { }.toMap() for full key-value transformation.
How do you merge two maps in Kotlin?
Use the + operator. The right-side map wins on duplicate keys. For custom conflict resolution (like summing values), use Java’s merge() function or loop with getOrPut. The putAll() method also works for in-place merging on mutable maps.
Is HashMap thread-safe in Kotlin?
No. Standard HashMap and MutableMap are not thread-safe. For concurrent access across coroutines or threads, use ConcurrentHashMap from java.util.concurrent. It uses fine-grained locking internally for safe reads and writes.
What is the difference between HashMap and LinkedHashMap in Kotlin?
HashMap offers no ordering guarantee but uses slightly less memory. LinkedHashMap preserves insertion order through a doubly-linked list of entries. Both provide O(1) average lookup time. Kotlin defaults to LinkedHashMap for all standard factory functions.
How do you filter a map by key or value in Kotlin?
Use filterKeys { } to check only keys, or filterValues { } for values. The general filter { } function gives access to both. All three return a new read-only map. The original stays unchanged, which fits well with functional programming patterns.
Conclusion
Maps in Kotlin give you a type-safe, expressive way to work with key-value pairs across any JVM project. Whether you’re building with mapOf for read-only access or mutableMapOf for dynamic state, the collection framework handles the heavy lifting.
Picking between HashMap, LinkedHashMap, and TreeMap comes down to your ordering needs and performance profile. Defaults are solid. Override them only when profiling tells you to.
Functions like filterKeys, mapValues, and getOrPut` keep transformation logic clean. Pair those with destructuring declarations during iteration, and your code stays readable without extra boilerplate.
For concurrent scenarios, reach for ConcurrentHashMap or protect access with a Mutex in coroutine scopes. Thread safety isn’t optional when shared mutable state is involved.
Start with immutable maps. Mutate only when you have to. That single habit prevents most collection bugs before they happen.
- CSS Cheat Sheet - May 18, 2026
- How to Set Up VSCode for Python Development - May 16, 2026
- How Using One Platform Can Simplify Order Fulfillment - May 15, 2026



