# 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 usingkeys
and all the values usingvalues
. Callingkeys
produces aSet
because all keys in aMap
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 Int
s with String
s. 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 {
map.getValue('b')
} 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.
- -
Map
s 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.