Scala - Map (Dictionary) with Examples

Scala provides multiple ways to instantiate maps. The default Map is immutable and uses a hash-based implementation.

Key Insights

  • Scala’s Map is an immutable collection by default, providing thread-safe key-value storage with efficient lookup operations in O(1) average time complexity
  • The language offers both immutable (scala.collection.immutable.Map) and mutable (scala.collection.mutable.Map) implementations, with immutable being the default and recommended choice
  • Maps support functional operations like map, filter, flatMap, and fold, enabling declarative data transformations without explicit loops

Creating Maps

Scala provides multiple ways to instantiate maps. The default Map is immutable and uses a hash-based implementation.

// Using arrow notation (most common)
val capitals = Map("USA" -> "Washington DC", "France" -> "Paris", "Japan" -> "Tokyo")

// Using tuple notation
val populations = Map(("USA", 331000000), ("France", 67000000), ("Japan", 126000000))

// Empty map with type specification
val emptyMap: Map[String, Int] = Map.empty

// Creating mutable map
import scala.collection.mutable
val mutableMap = mutable.Map("key1" -> "value1", "key2" -> "value2")

The arrow notation -> is syntactic sugar for creating tuples. Both approaches are equivalent, but the arrow notation is more idiomatic.

Accessing Values

Retrieving values from maps requires handling the possibility of missing keys. Scala provides several approaches with different trade-offs.

val scores = Map("Alice" -> 95, "Bob" -> 87, "Carol" -> 92)

// Using apply() - throws NoSuchElementException if key doesn't exist
val aliceScore = scores("Alice") // 95
// val danScore = scores("Dan") // Runtime exception

// Using get() - returns Option[V]
val bobScore: Option[Int] = scores.get("Bob") // Some(87)
val danScore: Option[Int] = scores.get("Dan") // None

// Using getOrElse() - provides default value
val carolScore = scores.getOrElse("Carol", 0) // 92
val eveScore = scores.getOrElse("Eve", 0) // 0

// Pattern matching with Option
scores.get("Alice") match {
  case Some(score) => println(s"Score: $score")
  case None => println("Student not found")
}

The get() method returning Option is the safest approach, forcing you to handle the missing key case explicitly.

Adding and Updating Elements

Immutable maps return new instances when modified. Mutable maps update in place.

// Immutable map operations
val original = Map("a" -> 1, "b" -> 2)

// Adding single element
val withC = original + ("c" -> 3)
// original: Map("a" -> 1, "b" -> 2)
// withC: Map("a" -> 1, "b" -> 2, "c" -> 3)

// Adding multiple elements
val withMultiple = original ++ Map("d" -> 4, "e" -> 5)

// Updating existing key (creates new map)
val updated = original + ("a" -> 10)
// updated: Map("a" -> 10, "b" -> 2)

// Mutable map operations
import scala.collection.mutable
val mutable = mutable.Map("x" -> 1, "y" -> 2)

mutable += ("z" -> 3) // Modifies in place
mutable("x") = 10 // Updates existing key
mutable.put("w", 4) // Returns Option[V] of previous value

For immutable maps, use updated() method for clarity when modifying existing keys:

val map1 = Map("a" -> 1, "b" -> 2)
val map2 = map1.updated("a", 100)

Removing Elements

Removal operations follow the same immutable/mutable pattern as additions.

val data = Map("name" -> "John", "age" -> "30", "city" -> "NYC")

// Immutable removal
val withoutAge = data - "age"
val withoutMultiple = data -- Seq("age", "city")

// Mutable removal
import scala.collection.mutable
val mutableData = mutable.Map("name" -> "John", "age" -> "30", "city" -> "NYC")

mutableData -= "age" // Removes in place
mutableData.remove("city") // Returns Option[V]
mutableData.retain((k, v) => k != "name") // Keeps only matching elements

Iterating and Transforming

Maps support standard collection operations, treating entries as tuples.

val inventory = Map("apples" -> 50, "oranges" -> 30, "bananas" -> 45)

// Iterating with foreach
inventory.foreach { case (fruit, quantity) =>
  println(s"$fruit: $quantity")
}

// Mapping over entries
val doubled = inventory.map { case (k, v) => k -> (v * 2) }

// Mapping only values
val increased = inventory.mapValues(_ + 10) // Deprecated in 2.13+
val increasedNew = inventory.view.mapValues(_ + 10).toMap // 2.13+ approach

// Filtering
val highStock = inventory.filter { case (_, quantity) => quantity > 40 }
val lowStockKeys = inventory.filter(_._2 < 40).keys

// Collecting with partial function
val selected = inventory.collect {
  case (fruit, qty) if qty > 35 => fruit.toUpperCase -> qty
}

Checking Existence and Properties

Verify key presence and map characteristics efficiently.

val config = Map("timeout" -> "30", "retries" -> "3", "debug" -> "true")

// Check key existence
val hasTimeout = config.contains("timeout") // true
val hasPort = config.contains("port") // false

// Check if empty
val isEmpty = config.isEmpty // false
val isNonEmpty = config.nonEmpty // true

// Size
val size = config.size // 3

// Check if value exists
val hasDebugTrue = config.exists { case (_, v) => v == "true" }

// Check if all entries satisfy condition
val allStrings = config.forall { case (_, v) => v.isInstanceOf[String] }

Advanced Operations

Combine maps, group data, and perform aggregations using functional operations.

// Merging maps
val map1 = Map("a" -> 1, "b" -> 2)
val map2 = Map("b" -> 3, "c" -> 4)

val merged = map1 ++ map2 // map2 values win: Map("a" -> 1, "b" -> 3, "c" -> 4)

// Custom merge logic
val customMerge = map1 ++ map2.map { case (k, v) =>
  k -> (map1.getOrElse(k, 0) + v)
}
// Result: Map("a" -> 1, "b" -> 5, "c" -> 4)

// Grouping a list into a map
val words = List("apple", "apricot", "banana", "blueberry", "cherry")
val grouped = words.groupBy(_.head)
// Map('a' -> List("apple", "apricot"), 'b' -> List("banana", "blueberry"), ...)

// Folding over map
val scores = Map("Alice" -> 95, "Bob" -> 87, "Carol" -> 92)
val totalScore = scores.foldLeft(0) { case (acc, (_, score)) => acc + score }

// Converting to other collections
val keysList = scores.keys.toList
val valuesList = scores.values.toList
val tuplesList = scores.toList
val tupleSeq = scores.toSeq

Pattern Matching with Maps

Destructure maps in pattern matching contexts for elegant conditional logic.

def processConfig(config: Map[String, String]): String = config match {
  case m if m.isEmpty => "No configuration"
  case m if m.contains("error") => s"Error: ${m("error")}"
  case m => s"Valid config with ${m.size} entries"
}

// Extracting specific keys
val settings = Map("host" -> "localhost", "port" -> "8080")

val result = settings match {
  case map if map.get("host").contains("localhost") && map.get("port").isDefined =>
    s"Local server on port ${map("port")}"
  case _ => "Remote server"
}

Performance Characteristics

Understanding map implementations helps choose the right type for your use case.

// HashMap - default, O(1) average lookup
val hashMap = Map("a" -> 1, "b" -> 2)

// TreeMap - sorted keys, O(log n) lookup
import scala.collection.immutable.TreeMap
val treeMap = TreeMap("c" -> 3, "a" -> 1, "b" -> 2)
println(treeMap.keys.toList) // List("a", "b", "c") - sorted

// ListMap - preserves insertion order, O(n) lookup
import scala.collection.immutable.ListMap
val listMap = ListMap("first" -> 1, "second" -> 2, "third" -> 3)

// LinkedHashMap - mutable, preserves insertion order
import scala.collection.mutable.LinkedHashMap
val linkedMap = LinkedHashMap("x" -> 1, "y" -> 2, "z" -> 3)

Use TreeMap when you need sorted keys, ListMap or LinkedHashMap when insertion order matters, and stick with the default Map for general use cases prioritizing lookup performance.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.