Kotlin from the ground up

Key and value maps

Looking a value up by name instead of by position, what a missing key gives back, walking the pairs, and counting with a mutable map.

Part 9 of 12

Advanced17 min readBack to Courses

A map looks a value up by name

A list is read by position, which is no help when you have a name and want the thing that goes with it. A map holds pairs: a key and the value it leads to. mapOf("bolt" to 12, "nut" to 40) makes two pairs, and to is the little function that builds one. The key is what you ask with; the value is what comes back.

Keys are unique. Writing the same key twice is not an error and it is not two entries: the second value replaces the first, which is worth knowing before you build a map from a loop. The replacement keeps the key’s original position in the printed order, so a duplicate changes a value without moving the entry.

A lookup can come back empty

stock["bolt"] gives the value for that key. stock["screw"] gives null, because there is no such key — and that is the type system warning you before it can bite: the answer of a lookup may be missing, and chapter ten is about handling that honestly. The brackets are not the same as getValue, which throws when the key is absent.

When you have a sensible fallback, say it in the call: getOrDefault("screw", 0) gives the stored value if the key is there and your fallback if it is not. It reads better than a branch, and it makes the missing case part of the answer rather than an afterthought. The fallback is evaluated even when the key exists, so keep it cheap.

Walking the pairs, and counting

The loop for ((part, count) in stock) pulls each pair apart as it goes, so part is the key and count is the value. That destructuring in the loop header is the usual way to read a whole map, and keys and values are there when you only need one side. The order of the walk follows the order the keys were added.

A mutableMapOf can be changed: basket["pears"] = 3 adds or replaces, and the counting idiom basket[key] = basket.getOrDefault(key, 0) + 1 tallies how often something appears. That is the shape behind every word count and every small report. The first sighting starts from zero because the fallback supplies it; later sightings add to the stored number.

Keys have to behave

A map finds a key by hashing it and then comparing it with the entries in that bucket. The hash must not change while the key is stored, or the map will look in the wrong place and report the key as missing. Strings and numbers are safe keys because their value is fixed; a mutable list is not, because its contents — and therefore its hash — can change.

Two keys that are equal must also have the same hash, which is why a data class is a good key and a plain class usually is not. If you write your own key type, let the compiler generate equals and hashCode together; changing one without the other is how a map starts losing entries it still contains.

Look up, walk, and add up

fun main() {
    val stock = mapOf("bolt" to 12, "nut" to 40)

    println(stock["bolt"])                   // 12
    println(stock["screw"])                  // null
    println(stock.getOrDefault("screw", 0))  // 0
    println(stock.size)                      // 2

    for ((part, count) in stock) {
        println(part + ": " + count)
    }

    val basket = mutableMapOf("apples" to 2)
    basket["pears"] = 3
    basket["apples"] = basket.getOrDefault("apples", 0) + 1
    println(basket)   // {apples=3, pears=3}
}

A small parts bin

Run it and find the line where a missing key is looked up. Then add a key of your own, print the whole map, and use getOrDefault with a fallback that suits your new key.

Output
Click "Run" to execute your code

A map is a lookup table

A map stores entries in a structure that finds a key without walking every pair. It hashes the key, uses that number to pick a bucket, and then compares the key with the entries there using equality. That is why keys must behave: a key whose hash changes after it is stored can no longer be found. Strings and numbers are safe keys; a mutable list is not.

Kotlin keeps the insertion order of mapOf and mutableMapOf, so printing a map follows the order the keys were first added. Do not confuse that convenience with a guarantee about all maps: a HashMap in another library may reorder its keys. The lookup itself has no order — stock["bolt"] asks one question and gets one answer, or null when the key is absent.

Three map mistakes are common. map[key] returns a nullable value and hands back null for a missing key; getValue(key) throws instead, so use it only when absence is a bug. Writing the same key twice replaces the value rather than creating a second entry. And reading a missing key is not the same as adding one — only the assignment map[key] = value changes the map.

Try these next

  • Count the words in a sentence with a mutable map, then print the pairs in insertion order.
  • Look up a key that is absent with [], getOrDefault and getValue, and describe the three answers.
  • Build a map of names to scores, then print only the entries above a threshold by filtering the pairs.

Count the words, then read the totals

Run it and find the word with the highest count. Then add a second sentence, count a word that does not appear, and print the map size before and after the loop.

Output
Click "Run" to execute your code

Both this course and the runtime keep a map in the order the keys were first added, so the printed lines follow the order you wrote. A map in another language may reorder its keys — treat the order as a convenience here, not a promise to rely on.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does a map lookup give for a key that is not in the map?