# Maps

A Map connects keys to values and looks up a value when given a key.

You create a Map by providing key-value pairs to mapOf(). Using to, we separate each key from its associated value:

// Maps/Maps.kt
import atomictest.eq

fun main() {
  val constants = mapOf(
    "Pi" to 3.141,
    "e" to 2.718,
    "phi" to 1.618
  constants eq
    "{Pi=3.141, e=2.718, phi=1.618}"

  // Look up a value from a key:
  constants["e"] eq 2.718              // [1]
  constants.keys eq setOf("Pi", "e", "phi")
  constants.values eq "[3.141, 2.718, 1.618]"

  var s = ""
  // Iterate through key-value pairs:
  for (entry in constants) {           // [2]
    s += "${entry.key}=${entry.value}, "
  s eq "Pi=3.141, e=2.718, phi=1.618,"

  s = ""
  // Unpack during iteration:
  for ((key, value) in constants)      // [3]
    s += "$key=$value, "
  s eq "Pi=3.141, e=2.718, phi=1.618,"
  • [1] The [] operator looks up a value using a key. You can produce all the keys using keys and all the values using values. Calling keys produces a Set because all keys in a Map must be unique, otherwise you’d have ambiguity during a lookup.
  • [2] Iterating through a Map produces key-value pairs as map entries.
  • [3] You can unpack keys and values as you iterate.

A plain Map is read-only. Here’s a MutableMap:

// Maps/MutableMaps.kt
import atomictest.eq

fun main() {
  val m =
    mutableMapOf(5 to "five", 6 to "six")
  m[5] eq "five"
  m[5] = "5ive"
  m[5] eq "5ive"
  m += 4 to "four"
  m eq mapOf(5 to "5ive",
    4 to "four", 6 to "six")

map[key] = value adds or changes the value associated with key. You can also explicitly add a pair by saying map += key to value.

mapOf() and mutableMapOf() preserve the order in which the elements are put into the Map. This is not guaranteed for other types of Map.

A read-only Map doesn’t allow mutations:

// Maps/ReadOnlyMaps.kt
import atomictest.eq

fun main() {
  val m = mapOf(5 to "five", 6 to "six")
  m[5] eq "five"
  // m[5] = "5ive" // Fails
  // m += (4 to "four") // Fails
  m + (4 to "four") // Doesn't change m
  m eq mapOf(5 to "five", 6 to "six")
  val m2 = m + (4 to "four")
  m2 eq mapOf(
    5 to "five", 6 to "six", 4 to "four")

The definition of m creates a Map associating Ints with Strings. If we try to replace a String, Kotlin emits an error.

An expression with + creates a new Map that includes both the old elements and the new one, but doesn’t affect the original Map. The only way to “add” an element to a read-only Map is by creating a new Map.

A Map returns null if it doesn’t contain an entry for a given key. If you need a result that can’t be null, use getValue() and catch NoSuchElementException if the key is missing:

// Maps/GetValue.kt
import atomictest.*

fun main() {
  val map = mapOf('a' to "attempt")
  map['b'] eq null
  capture {
  } eq "NoSuchElementException: " +
    "Key b is missing in the map."
  map.getOrDefault('a', "??") eq "attempt"
  map.getOrDefault('b', "??") eq "??"

getOrDefault() is usually a nicer alternative to null or an exception.

You can store class instances as values in a Map. Here’s a map that retrieves a Contact using a number String:

// Maps/ContactMap.kt
package maps
import atomictest.eq

class Contact(
  val name: String,
  val phone: String
) {
  override fun toString(): String {
    return "Contact('$name', '$phone')"

fun main() {
  val miffy = Contact("Miffy", "1-234-567890")
  val cleo = Contact("Cleo", "098-765-4321")
  val contacts = mapOf(
    miffy.phone to miffy,
    cleo.phone to cleo)
  contacts["1-234-567890"] eq miffy
  contacts["1-111-111111"] eq null

It’s possible to use class instances as keys in a Map, but that’s trickier so we discuss it later in the book.

  • -

Maps look like simple little databases. They are sometimes called associative arrays, because they associate keys with values. Although they are quite limited compared to a full-featured database, they are nonetheless remarkably useful (and far more efficient than a database).

Exercises and solutions can be found at www.AtomicKotlin.com.