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, andfold, 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.