1. Previous: Chapter 8
  2. Next: Chapter 10
Kotlin: An Illustrated Guide • Chapter 9

Collections: Maps

Chapter cover image

In the last chapter, we saw how collections, such as lists and sets, can be used to do things that couldn’t be done easily with separate, individual variables.

In this chapter, we’re going to look at another kind of collection—a map, which can hold many associations between values.

Let’s check it out!

The Right Tool for the Job

“You gotta use the right tool for the job.” That’s what Mr. Patchwell taught his young son, Jack, who was just starting to learn how to become a handyman like his old man. “When you’ve got a nail, you need to use a hammer, not a screwdriver.”

In order to help Jack pick out the right tool, he sketched out a table of the different hardware and tools in his toolbox.

A two-column table of tools - hardware in the left column, tools in the right column. Hardware Tool to Use Nail Hammer Hex Nut Wrench Hex Bolt Wrench Slotted Screw Slotted Screwdriver Phillips Screw Phillips Screwdriver

“Now, with this table, you can easily look up what tool you need. Just scan down the left-hand column for the hardware you need to work with, and then scan across to see the right tool to use.”

How to scan a table. (Easy stuff!) Hardware Tool to Use Nail Hammer Hex Nut Wrench Hex Bolt Wrench Slotted Screw Slotted Screwdriver Phillips Screw Phillips Screwdriver Which tool should be used with a Slotted Screw? A Slotted Screwdriver!

Tables like this show that there are associations between things—a nail is associated with a hammer, a hex nut is associated with a wrench, and so on. In this chapter, we’ll build out Kotlin’s equivalent of a table like this, but before we do, let’s start by creating a single association.

Associating Data

One simple way to associate two values is to use a class called Pair. The constructor of this class has two parameters. You can call its constructor with any two objects, regardless of their types. In our case, let’s associate two String objects—one for a nail, and one for a hammer.

val association = Pair("Nail", "Hammer")

Pair is a very simple class that has two properties, first and second, which you can use to get the values back out of it.

UML diagram showing the Pair class and its two properties. Pair<A,B> + first: A + second: B

To the right is a UML diagram showing the Pair class. The types of first and second depend on the types of the arguments you give it when calling its constructor, so in this diagram, we will just use A and B as placeholders for the actual types.

The first property will be whatever the first argument was when you called the constructor, and the second will be whatever the second argument was.

println(association.first)  // Nail
println(association.second) // Hammer

Now, instead of calling the constructor of the Pair class, it’s sometimes more natural to use a function named to(), which will call the Pair constructor for you. This function can be called on any object at all. Let’s update our code so that it uses the to() function.

val association = "Nail".to("Hammer")

When reading this code, we could say, “When I have a Nail, then I should go to a Hammer.” Both Listing 9.1 and Listing 9.3 do the same thing—they create a Pair where the left-hand value is assigned to the property named first and the right-hand value is assigned to the property named second.

The to() function also has a special characteristic about it that lets us use it without the punctuation! So, we can also create this same Pair like this.

val association = "Nail" to "Hammer"

Notice that this is the same as Listing 9.3 above, except that the dot and both parentheses are missing. When a function can be called this way, it’s known as an infix function. We won’t see infix functions often, but it’s important to know that they exist so that they won’t confuse you when you see code like this.

So far, we’ve used type inference so that we don’t have to write out the type of the association variable. As with List and Set in the previous chapter, the type of a Pair variable depends on the type of the things that it contains. Since both "Nail" and "Hammer" have type String, the type of the association variable is Pair<String, String>.

How to construct the type of a Pair variable. Pair<String, String> Type of the first thing Type of the second thing

And of course, we can specify the type explicitly like this.

val association: Pair<String, String> = "Nail" to "Hammer"

Now that we’ve successfully made a single association, we can do the same thing for the rest of the tools… and then put them all together into a map!

Map Fundamentals

Let’s look at Mr. Patchwell’s table again.

The table again. Hardware Tool to Use Nail Hammer Hex Nut Wrench Hex Bolt Wrench Slotted Screw Slotted Screwdriver Phillips Screw Phillips Screwdriver

In Kotlin, a table like this is called a map. This might bring to mind maps like street maps and treasure maps, but that’s not what we’re talking about here.

The term comes from the world of mathematics, where a map defines a correspondence between elements of sets. Similarly, Kotlin maps define an association between each item in the left-hand column, and its corresponding item in the right-hand column.

Before we create our first map, let’s cover a few important terms. The items in the left-hand column of the table are called keys and the items in the right-hand column are called values.

Keys and values in a table. Hardware Tool to Use Nail Hammer Hex Nut Wrench Hex Bolt Wrench Slotted Screw Slotted Screwdriver Phillips Screw Phillips Screwdriver Keys Values

The association of a key and a value within a map is called a map entry.

Entries in a table. Hardware Tool to Use Nail Hammer Hex Nut Wrench Hex Bolt Wrench Slotted Screw Slotted Screwdriver Phillips Screw Phillips Screwdriver Entries

There’s an important rule to keep in mind—each key in a map is unique. However, the values can be duplicated. In other words, we cannot have duplicate items in the left-hand column, but it’s fine in the right-hand column. In the table below, notice that the left-hand column items are unique, but the wrench appears twice in the right-hand column.

The tool table, showing that no duplicates are allowed in the left-hand column, but they're allowed in the right-hand column. Hardware Tool to Use Nail Hammer Hex Nut Wrench Hex Bolt Wrench Slotted Screw Slotted Screwdriver Phillips Screw Phillips Screwdriver Duplicated value No duplicates in this column Duplicates allowed in this column

Creating a Map with mapOf()

Now that we understand the main concepts, it’s time to create our first map! To do this, we’ll use the mapOf() function, passing in a Pair for each association that we want in the map.

val toolbox = mapOf(
    "Nail" to "Hammer",
    "Hex Nut" to "Wrench",
    "Hex Bolt" to "Wrench",
    "Slotted Screw" to "Slotted Screwdriver",
    "Phillips Screw" to "Phillips Screwdriver",
)

The mapOf() function looks similar to listOf() and setOf() that we saw in the last chapter, except that all of the elements here have two pieces—the key and the value—which are joined together in a Pair.

We can see the similarities between the Kotlin map and Mr. Patchwell’s table when we place them side by side.

Similarities between the Kotlin code to create a map, and a hand-drawn table. val toolbox = mapOf ( "Nail" to "Hammer" , "Hex Nut" to "Wrench" , "Hex Bolt" to "Wrench" , "Slotted Screw" to "Slotted Screwdriver" , "Phillips Screw" to "Phillips Screwdriver" ) Hardware Tool to Use Nail Hammer Hex Nut Wrench Hex Bolt Wrench Slotted Screw Slotted Screwdriver Phillips Screw Phillips Screwdriver

Just as with lists, sets, and other objects, we can use println() to print out the contents of a map.

println(toolbox)

When we print this out, the entries of the map appear between braces. The keys are to the left of the equal signs, and the values are to the right.

{Nail=Hammer, Hex Nut=Wrench, Hex Bolt=Wrench, Slotted Screw=Slotted Screwdriver, Phillips Screw=Phillips Screwdriver}

Just like with Pair, the type of a Map variable depends on the type of the key and the type of the value.

How to construct the type of a Map variable. Map<String, String> Type of the key Type of the value

So, we could write out the type explicitly like this.

val toolbox: Map<String, String> = mapOf(
    "Nail" to "Hammer",
    "Hex Nut" to "Wrench",
    "Hex Bolt" to "Wrench",
    "Slotted Screw" to "Slotted Screwdriver",
    "Phillips Screw" to "Phillips Screwdriver",
)

Looking Up a Value

The most common thing that we’ll need to do with a map is to look up a value. When Jack has a nail, for example, he needs to look up which tool to use. Just as Jack would find the nail in the left-hand column and find the corresponding tool next to it, Kotlin can give us a value when we provide it a key. We can use the get() function to do this.

val tool = toolbox.get("Nail")
println(tool) // Hammer

Similar to lists, we can also use the indexed access operator with maps to get a value.

val tool = toolbox["Nail"]
println(tool) // Hammer

If we call get() (or use the indexed access operator) with a key that does not exist in the map, it will return a null. This means the get() function returns a nullable type rather than a non-nullable type! In Listing 9.9 and 9.10, it returns a String? rather than a String. We can use the null-safety tools that we learned about in Chapter 6 (such as the elvis operator) to get it back to a non-nullable type.

Alternatively, we can call getValue() instead of get(). getValue() will return a non-nullable type, but be warned—if you give it a key that does not exist, you’ll see an error message and your code will stop running.

val tool = toolbox.getValue("Nail")
println(tool) // Hammer

val anotherTool = toolbox.getValue("Wing Nut") // Error at runtime

We can also use getOrDefault() to provide a default value if the key doesn’t exist. If Mr. Patchwell doesn’t have a tool for a particular piece of hardware, he’ll just need to tighten it by hand!

val tool = toolbox.getOrDefault("Hanger Bolt", "Hand")

Modifying a Map

As with the other collection types, maps come in two flavors of mutability—MutableMap and an immutable Map. The mutable variety allows us to change its contents, whereas an immutable map requires us to create a new map instance that you can assign to a new or existing variable.

Let’s look at how to change a MutableMap first. To start with, we’ll need to use mutableMapOf() to create the map, instead of just mapOf(), which we used back in Listing 9.6.

val toolbox = mutableMapOf(
    "Nail" to "Hammer",
    "Hex Nut" to "Wrench",
    "Hex Bolt" to "Wrench",
    "Slotted Screw" to "Slotted Screwdriver",
    "Phillips Screw" to "Phillips Screwdriver",
)

To add a new entry, we can use the put() function, where the first argument is the key, and the second argument is the value.

toolbox.put("Lumber", "Saw")

Just like with the get() function, though, Kotlin developers typically use the indexed access operator instead of calling the put() function directly. The following code accomplishes the same thing as Listing 9.14.

toolbox["Lumber"] = "Saw"

It’s also possible to change an existing value exactly the same way. Just provide a key that already exists.

toolbox["Hex Bolt"] = "Nut Driver"

Finally, we can remove an entry using the remove() function.

toolbox.remove("Lumber")

Note that although we can change a value, we cannot change a key. Instead, we have to remove a key and insert a new entry.

toolbox.remove("Phillips Screw")
toolbox["Cross Recess Screw"] = "Phillips Screwdriver"

Immutable Maps

As with immutable lists and sets, we can use the plus and minus operators on an immutable map. Remember, doing so will create new map instances, which would then typically be assigned to a variable.

The following code demonstrates the same operations as we did above, but on an immutable map.

var toolbox = mapOf(
    "Nail" to "Hammer",
    "Hex Nut" to "Wrench",
    "Hex Bolt" to "Wrench",
    "Slotted Screw" to "Slotted Screwdriver",
    "Phillips Screw" to "Phillips Screwdriver",
)

// Add an entry
toolbox = toolbox + Pair("Lumber", "Saw")

// Update an entry
toolbox = toolbox + Pair("Hex Bolt", "Nut Driver")

// Remove an entry
toolbox = toolbox - "Lumber"

// Simulate changing a key
toolbox = toolbox - "Phillips Screw"
toolbox = toolbox + Pair("Cross Recess Screw", "Phillips Screwdriver")

Map Operations

As with List and Set, Map objects have operations that can be performed on them, and some of them will look very familiar! Let’s start with the forEach() function.

forEach()

The forEach() function is almost identical to the one found on List and Set objects. It takes a lambda that we can use to do something with each entry in the map. Since maps store entries, the parameter of the lambda will be of type Map.Entry.1

UML class diagram for the Map.Entry class. Map.Entry<K,V> + key: K + value: V

Map.Entry is very similar to the Pair class that we looked at earlier in this chapter—it has two properties on it, but instead of being named first and second, they’re named key and value.

Here’s how we can use the forEach() function on a Map.

toolbox.forEach { entry -> 
    println("Use a ${entry.value} on a ${entry.key}") 
}

When we run this code, here’s what we’ll see.

Use a Hammer on a Nail
Use a Wrench on a Hex Nut
Use a Wrench on a Hex Bolt
Use a Slotted Screwdriver on a Slotted Screw
Use a Phillips Screwdriver on a Phillips Screw

Because it’s so similar to the forEach() function that we saw in the last chapter, it should be easy to identify the main parts. Here they are.

Breakdown of the forEach() function. Lambda parameter Code to run for each entry toolbox. forEach { entry -> println ( " $ {entry. key } and $ {entry. value } " ) } Map variable

Filtering

Similar to lists and sets, we can filter a map.

Keep in mind that, just as we saw with lists, this function doesn’t modify an existing map—it creates a new map instance, so you’ll probably want to assign the result to a variable. Let’s filter down the toolbox to just screwdrivers.

val screwdrivers = toolbox.filter { entry -> 
    entry.value.contains("Screwdriver")
}

The result is a new Map that contains only the screwdrivers.

The effect of using `filter()` on a map. Hardware Tool to Use Nail Hammer Hex Nut Wrench Hex Bolt Wrench Slotted Screw Slotted Screwdriver Phillips Screw Phillips Screwdriver Hardware Tool to Use Slotted Screw Slotted Screwdriver Phillips Screw Phillips Screwdriver

In this case, we filtered on the value, but it’s just as easy to filter by the keys.

val screwdrivers = toolbox.filter { entry ->
    entry.key.contains("Screw")
}

Mapping

Yes, we can map a Map! Simply use the mapKeys() and mapValues() functions to convert its keys or values. Just like with the collection operations we looked at in the last chapter, it’s possible to create an operation chain. Let’s map both keys and values in one chain.

val newToolbox = toolbox
    .mapKeys { entry -> entry.key.replace("Hex", "Flange") }
    .mapValues { entry -> entry.value.replace("Wrench", "Ratchet") }
The effect of using the `mapKeys()` and `mapValues()` functions. Hardware Tool to Use Nail Hammer Hex Nut Wrench Hex Bolt Wrench Slotted Screw Slotted Screwdriver Phillips Screw Phillips Screwdriver Hardware Tool to Use Nail Hammer Flange Nut Ratchet Flange Bolt Ratchet Slotted Screw Slotted Screwdriver Phillips Screw Phillips Screwdriver

Map objects have many other operations on them, which you can explore in Kotlin’s API docs. However, there’s one more operation that we’ll examine before continuing—withDefault().

Setting Default Values

As we saw earlier, we can use getOrDefault() to gracefully handle cases where the key does not exist. However, this can quickly get out of control if we use the same default every time!

val tool = toolbox.getOrDefault("Hanger Bolt", "Hand")
val another = toolbox.getOrDefault("Dowel Screw", "Hand")
val oneMore = toolbox.getOrDefault("Eye Bolt", "Hand")

Instead, we can use an operation named withDefault(), which will return a new map based on the original. In this new map, whenever we call getValue() with a key that doesn’t exist, it will invoke a lambda and return the result. Here’s how it looks.

toolbox = toolbox.withDefault { key -> "Hand" }

Now, instead of providing the default every time we try to get a value (as done in Listing 9.24 above), we can just call getValue() normally.

This is great, because if we ever want to change the default, we can make the change in one spot instead of many!

val tool = toolbox.getValue("Hanger Bolt")
val anotherTool = toolbox.getValue("Dowel Screw")
val oneMoreTool = toolbox.getValue("Eye Bolt")

Keep in mind that this works with getValue() but not with get() or the indexed access operator, which will continue to return null if the key is not found!

Now you know how to create and change maps, get values out of them, and use collection operations on them. But things get really fun when maps are used in conjunction with other collections! Let’s look at that next.

Creating a Map from a List

We’ve created maps by hand using the mapOf() function. It’s also possible to create maps that are based on existing list or set collections. With just a few important functions, we can slice and dice our data in many different ways! In order to do this, of course, we will need a list to start with.

Instead of using a simple String to represent the tools in Mr. Patchwell’s toolbox, let’s create a class, so that it can hold the name of the tool, its weight in ounces, and the corresponding hardware that it works with.

class Tool(
    val name: String, 
    val weightInOunces: Int,
    val hardware: String,
)

Now, let’s create a list of Tool objects, so that they include the tools from Mr. Patchwell’s toolbox.

val tools = listOf(
    Tool("Hammer", 14, "Nail"),
    Tool("Wrench", 8, "Hex Nut"),
    Tool("Wrench", 8, "Hex Bolt"),
    Tool("Slotted Screwdriver", 5, "Slotted Screw"),
    Tool("Phillips Screwdriver", 5, "Phillips Screw"),
)

Now that we have a list, we’re ready to create some maps from it!

Associating Properties from a List of Objects

We can use the associate() function to create a map from a list of objects. To start with, let’s use associate() to create a map similar to the one in Listing 9.6.

val toolbox = tools.associate { tool ->
    tool.hardware to tool.name
}

Hopefully you’re starting to feel more comfortable with collection operations at this point. For each element in the list, the associate() function will invoke the lambda given to it. The lambda returns a key-value Pair, which contains the key and value that should be included in the resulting map.

Here’s a breakdown of the associate() function.

Describes the different parts of the code when using the associate() function. Lambda parameter Pair to insert as an entry in the new map. val toolbox = tools. associate { tool -> tool. hardware to tool. name } Original list variable New map variable

And here’s the effect that it has in Listing 9.29.

The effect of calling the associate() function. name Hammer weightInOunces 14 hardware Nail name Wrench weightInOunces 8 hardware Hex Nut name Wrench weightInOunces 8 hardware Hex Bolt name Slotted Screwdriver weightInOunces 5 hardware Slotted Screw name Phillips Screwdriver weightInOunces 5 hardware Slotted Screw List<Tool> Key Value Nail Hammer Hex Nut Wrench Hex Bolt Wrench Slotted Screw Slotted Screwdriver Phillips Screw Phillips Screwdriver Map<String,String>

Often, the number of elements in the resulting map will be the same as the number of elements in the original list. In some cases, it could have fewer. Because the keys in a map are all unique, if we try to add a key that already exists, it will overwrite the existing value.

For example, let’s reverse the key and value in the lambda in Listing 9.29, so that the tool name is the key, and the hardware is the value.

val toolbox = tools.associate { tool ->
    tool.name to tool.hardware
}

The original list has two Tool objects with a name of "Wrench", so when associate() encounters the first one, it’s added to the map, but when it encounters the second, it replaces the first value.

So, the resulting map only includes "Hex Bolt" rather than "Hex Nut", because of the two, "Hex Bolt" came last.

The effect of calling the associate() function when there would have been duplicate keys. Key Value Hammer Nail Wrench Hex Bolt Slotted Screwdriver Slotted Screw Phillips Screwdriver Phillips Screw Map<String,String> name Slotted Screwdriver weightInOunces 5 hardware Slotted Screw name Hammer weightInOunces 14 hardware Nail name Wrench weightInOunces 8 hardware Hex Nut name Wrench weightInOunces 8 hardware Hex Bolt name Phillips Screwdriver weightInOunces 5 hardware Slotted Screw List<Tool>

So in this case, there are fewer entries in the map than elements in the original list. That is, there are only 4 entries in the map, compared with 5 elements in the list.

Other Association Functions

There are a few other variations of the associate() function that are good to know. These are especially helpful when we want the original list element to be either the key or the value in the resulting map.

For example, if we want to create a map where the keys are the tool names and the value is the Tool object, we can use associateBy(). The lambda of this function returns just the key. The original list element itself will be the value.

val toolsByName = tools.associateBy { tool -> tool.name }

The effect of this function is shown in the following illustration.

The effect of calling the associateBy() function. Key Value Hammer Wrench Slotted Screwdriver Phillips Screwdriver name Hammer weightInOunces 14 hardware Nail name Wrench weightInOunces 8 hardware Hex Nut name Slotted Screwdriver weightInOunces 5 hardware Slotted Screw name Phillips Screwdriver weightInOunces 5 hardware Slotted Screw Map<String,Tool> name Slotted Screwdriver weightInOunces 5 hardware Slotted Screw name Hammer weightInOunces 14 hardware Nail name Wrench weightInOunces 8 hardware Hex Nut name Wrench weightInOunces 8 hardware Hex Bolt name Phillips Screwdriver weightInOunces 5 hardware Slotted Screw List<Tool>

With this map, we can easily get a tool by its name!

val hammer = toolsByName["Hammer"]

Inversely, when we want to create a map where the keys are the Tool object and the value is specified in the lambda, we can use the associateWith() function. The lambda of this function returns the value, and the original list element will be the key.

val toolWeightInPounds = tools.associateWith { tool ->
    tool.weightInOunces * 0.0625
}
The effect of calling the associateWith() function. Key Value 0.875 0.5 0.5 0.3125 0.3125 name Hammer weightInOunces 14 hardware Nail name Wrench weightInOunces 8 hardware Hex Nut name Slotted Screwdriver weightInOunces 5 hardware Slotted Screw name Phillips Screwdriver weightInOunces 5 hardware Slotted Screw name Wrench weightInOunces 8 hardware Hex Bolt Map<String,Tool> name Slotted Screwdriver weightInOunces 5 hardware Slotted Screw name Hammer weightInOunces 14 hardware Nail name Wrench weightInOunces 8 hardware Hex Nut name Wrench weightInOunces 8 hardware Hex Bolt name Phillips Screwdriver weightInOunces 5 hardware Slotted Screw List<Tool>

To get the weight of a hammer, we’d need to have a hammer object already.

val hammerWeightInPounds = toolWeightInPounds[hammer]

Grouping List Elements into a Map of Lists

Sometimes when you’ve got a list, you want to split it up into multiple smaller lists, based on some characteristic. For example, we can take the tools list and split it up by weight.

Tools grouped by weight in ounces. 14 oz each 8 oz each 5 oz each

To do this, we can use the groupBy() function. This function will run the provided lambda for each element in the list.

Elements for which the lambda returns the same result will be assembled into a list, and inserted into a map.

val toolsByWeight = tools.groupBy { tool ->
    tool.weightInOunces
}

The result is a Map with one list of tools that weigh 14 ounces, another list of tools that weigh 8 ounces, and a third list of tools that weigh 5 ounces.

The map’s key is the weight in ounces, and the map’s value is a list of tools that have that weight.

This illustration shows the effect that this operation has.

The effect of the calling the groupBy() function. name Hammer weightInOunces 14 hardware Nail name Wrench weightInOunces 8 hardware Hex Nut name Wrench weightInOunces 8 hardware Hex Bolt name Slotted Screwdriver weightInOunces 5 hardware Slotted Screw name Phillips Screwdriver weightInOunces 5 hardware Slotted Screw List<Tool> Key Value 14 8 5 name Hammer weightInOunces 14 hardware Nail List<Tool> name Wrench weightInOunces 8 hardware Hex Bolt name Wrench weightInOunces 8 hardware Hex Nut List<Tool> name Slotted Screwdriver weightInOunces 5 hardware Slotted Screw name Phillips Screwdriver weightInOunces 5 hardware Slotted Screw List<Tool> Map<Int, List<Tool

Here’s a breakdown of the groupBy() function.

Breakdown of the groupBy() function. Lambda parameter Elements that return the same thing here will be grouped together val toolsByWeight = tools. groupBy { tool -> tool. weightInOunces } Original list variable New map variable

In case you want something other than the original list element in the resulting lists, you can also call this function with a second argument. For that one, give it a lambda that returns whatever you want in the resulting list. For example, if you only want the names of the tools in those lists, you can do this:

val toolNamesByWeight = tools.groupBy(
    { tool -> tool.weightInOunces }, 
    { tool -> tool.name }
)

Here’s the effect that this operation has.

The effect of the calling the groupBy() function with two arguments. name Slotted Screwdriver weightInOunces 5 hardware Slotted Screw name Hammer weightInOunces 14 hardware Nail name Wrench weightInOunces 8 hardware Hex Nut name Wrench weightInOunces 8 hardware Hex Bolt name Phillips Screwdriver weightInOunces 5 hardware Slotted Screw List<Tool> Key Value 14 8 5 Wrench Wrench List<String> Hammer List<String> Slotted Screwdriver Phillips Screwdriver List<String> Map<Int, List<String

Summary

Enjoying this book?
Order the paperback today!

Kotlin: An Illustrated Guide is now available on Amazon See the book on Amazon

Jack is well on his way to becoming a great handyman like his father, and with maps in your programming toolbox, you’re ready to bolt together some key-value pairs like a pro!

Here’s what you learned in this chapter:

Well, we’ve learned a lot about collections over these past two chapters! Next, we’re going to explore some powerful concepts that make many of those collection operations possible. Get ready for receivers and extensions!


  1. This type has a dot in the name because it’s a type that’s defined within another type—Entry is defined within the Map type. ↩︎

Share this article:

  • Share on Facebook
  • Share on Reddit