What a hash map is
A hash map stores key-value pairs. You give it a key, and it gives back the value stored under that key, usually in constant time, no matter how many entries it holds. Each key appears at most once. Use a map whenever you need to look something up by name rather than by position: counting how many times each word appears, remembering which index a number was seen at, or translating text such as "two" into the number 2.
In Kotlin you normally work through the MutableMap<K, V> interface, a map you can change. On the JVM, the standard builders return hash-based maps, so lookups, insertions and removals take O(1) time on average. Writing the key and value types out, as in MutableMap<String, Int>, is optional when the compiler can infer them.
Creating a map
An empty map needs its types spelled out, because there is nothing to infer them from:
val digitValue: MutableMap<String, Int> = mutableMapOf()
You can also fill it at creation. Each pair is written key to value, and the types are inferred:
val digitValue = mutableMapOf(
"zero" to 0,
"one" to 1
)
Two other forms are common. HashMap<String, Int>() creates a hash map directly. mapOf(...) creates a read-only Map: you can look things up, but map["a"] = 2 doesn't compile. Prefer the read-only type when nothing should change.
Reading, adding, updating and removing
Square brackets read a value. Reading a key that isn't in the map does not throw; it gives null:
val digitValue = mutableMapOf("zero" to 0, "one" to 1)
println(digitValue["zero"]) // 0
println(digitValue["three"]) // null
println(digitValue.getOrDefault("three", 0)) // 0
println(digitValue["three"] ?: 0) // 0
Because a missing key gives null, the type of digitValue["zero"] is Int?, a nullable Int, not Int. That has consequences. val x: Int = digitValue["zero"] is a compile error (the compiler reports that the type is Int? but Int was expected), and so is digitValue["zero"] + 1. There are three ways out:
getOrDefault(key, fallback)returns the fallback for a missing key, and its type is a plainInt.- The elvis operator
?:does the same for any nullable value:map[key] ?: 0. map[key]!!asserts the value isn't null. If the key is missing, it throws aNullPointerException. Use it only after you've checked that the key exists.
Assigning through square brackets adds a new entry, or replaces the value of an existing key:
val digitValue = mutableMapOf("zero" to 0, "one" to 1)
digitValue["three"] = 3 // adds
digitValue["zero"] = 5 // replaces
println(digitValue) // {zero=5, one=1, three=3}
The assignment form is shorthand for put(key, value). The put call also returns the value that was there before, or null if the key was new.
remove(key) deletes an entry. It returns the removed value, and for a missing key it does nothing and returns null:
val digitValue = mutableMapOf("zero" to 0, "one" to 1)
digitValue.remove("zero")
println(digitValue) // {one=1}
println(digitValue.remove("nine")) // null
To test for a key, use containsKey(key) or the equivalent key in map:
println(digitValue.containsKey("one")) // true
println("nine" in digitValue) // false
Iterating
A for loop over map.keys visits each key, and you look up the value with brackets. A destructuring loop gives both at once:
val digitValue = mutableMapOf("zero" to 0, "one" to 1)
for (key in digitValue.keys) {
println("$key is ${digitValue[key]}")
}
for ((key, value) in digitValue) {
println("$key -> $value")
}
Both loops take O(n) time for a map of n entries. Inside a string, $key inserts a variable and ${...} inserts any expression, which is why the lookup needs braces.
Order
A hash map doesn't promise any particular order of keys, and code shouldn't depend on one. The maps built by mutableMapOf on the JVM happen to remember insertion order, so the loops above print zero before one. A map made with HashMap() makes no such promise. If the order matters to your logic, sort the keys yourself.
Common mistakes
- Treating
map[key]as anInt. It is anInt?. UsegetOrDefault,?:or a check first. - Using
!!without checking. It turns a missing key into aNullPointerException. - Using a read-only map for updates. A value of type
Maphas nosetoperator, somap[key] = valuedoesn't compile. Declare it asMutableMapor build it withmutableMapOf. - Expecting duplicate keys. Assigning to an existing key replaces its value. A second entry is never created.
- Expecting a particular order. Don't rely on it.
The lesson's exercise
The exercise builds a small fruit inventory and uses each operation once. The steps:
- Create
fruitsas aMutableMap<String, Int>usingmutableMapOf, with three pairs: apples 3, bananas 5, grapes 2. - Add 4 apples. Read the current count with
getOrDefault("apples", 0), add 4, and assign the result back with brackets. The default of 0 makes the line safe even if the key were missing. - Print "Out of pears" when
fruits.containsKey("pears")is false. The!negates the check. - Remove the bananas with
remove("bananas"). - Report every fruit. Loop over
fruits.keyswith the variablefruitand print a string template that includes${fruits[fruit]}and$fruit.
val fruits: MutableMap<String, Int> = mutableMapOf(
"apples" to 3,
"bananas" to 5,
"grapes" to 2
)
fruits["apples"] = fruits.getOrDefault("apples", 0) + 4
if (!fruits.containsKey("pears")) {
println("Out of pears")
}
fruits.remove("bananas")
for (fruit in fruits.keys) {
println("We have ${fruits[fruit]} $fruit.")
}
It prints:
Out of pears
We have 7 apples.
We have 2 grapes.
The line order of the last two lines follows the map's iteration order, which in this program is insertion order.
Counting with a map
The most common use of a map in problem solving is counting. Read the current count with a default of 0, add one, and store it back:
val counts = mutableMapOf<Char, Int>()
for (c in "banana") {
counts[c] = counts.getOrDefault(c, 0) + 1
}
println(counts) // {b=1, a=3, n=2}
The same pattern solves problems that ask for the frequency of letters, the first repeated value, or the index of a previously seen number.
Key takeaways
- A map holds key-value pairs, with unique keys. Lookup, insertion and removal are
O(1)on average. - Create one with
mutableMapOf(...), ormutableMapOf()with explicit types when it starts empty. map[key]returnsnullfor a missing key, so its type is nullable. UsegetOrDefaultor?:.map[key] = valueadds or replaces;remove(key)deletes;containsKey(key)orinchecks.- Don't rely on key order, and use a read-only
Mapwhen nothing should change.