Maps¶
A map is a collection of its entries, each a Tuple2<K, V>. get returns an Option; put and remove return a
new map.
| Type | Representation | Iteration order | Positional methods |
|---|---|---|---|
HashMap |
a compressed hash trie (CHAMP) | not defined | none |
LinkedHashMap |
a hash-based map that also records the insertion order | insertion order | yes |
TreeMap |
a sorted, balanced tree of entries | the key comparator's | yes |
The three implement the Map interface, and TreeMap also SortedMap. Both interfaces are sealed: these classes
are their only implementations.
HashMap is the structure of the immutable HashMap of the Scala 2.13 collections library, which Scala 3 uses
unchanged: a tree of nodes with up to 32 slots each, where five bits of a mix of the key's hash code pick the slot
at each level. A node stores its entries inline, keys and values side by side in one array, so an entry costs no
object of its own. Removing an entry folds the tree back, so equal maps have the same shape whatever order their
entries came in.
When to choose which¶
HashMapby default.LinkedHashMapwhen the order the keys were inserted in matters. Overwriting a key keeps its position. A key repeated inof,ofEntriesor a collector does the same: it stays where it first appeared, with its last value.TreeMapfor keys in sorted order, or the least and the greatest key.
var stock = HashMap.of("apple", 3, "pear", 0);
var restocked = stock.put("pear", 5, Integer::sum).put("fig", 1); // HashMap<String, Integer>
var pears = restocked.get("pear"); // Option<Integer>
var kiwis = restocked.getOrElse("kiwi", 0); // Integer
// pears is Some(5), kiwis is 0
var byName = TreeMap.of("b", 2, "a", 1, "c", 3);
var values = byName.values(); // Vector<Integer>
var first = byName.head(); // Tuple2<String, Integer>
// values is Vector(1, 2, 3), first is (a, 1)
Updating one key¶
updateWith reads a key and writes it in one call. Its function receives the key's value as an Option, None
when the key is absent, and returns the new one: Some puts that value, None removes the key.
var stock = HashMap.of("apple", 3, "pear", 1);
var sold = stock.updateWith("pear", count -> count.map(n -> n - 1).filter(n -> n > 0));
var added = stock.updateWith("fig", count -> Option.some(count.getOrElse(0) + 4));
// sold has no pear left, added has fig -> 4
When nothing changes, the same map comes back: None for an absent key, or Some of the very object the key
already holds. On a LinkedHashMap, an updated key keeps its position.
getOrElse also takes a Supplier, for a default that costs something to make. It runs only when the key is
absent.
var prices = HashMap.of("apple", 3);
var apple = prices.getOrElse("apple", () -> lookUpPrice("apple")); // 3, lookUpPrice is not called
var kiwi = prices.getOrElse("kiwi", () -> lookUpPrice("kiwi")); // what lookUpPrice returns
Putting many entries¶
putAll puts every entry of another map, or of any iterable of Tuple2s, as successive puts would. On a key both
sides hold, the argument's value wins: this is Scala's ++ on maps.
merge is the other way round: this map's value stays, and only the keys it lacks are added.
var prices = HashMap.of("apple", 3, "pear", 4);
var updates = HashMap.of("pear", 5, "fig", 2);
var updated = prices.putAll(updates); // HashMap<String, Integer>
var kept = prices.merge(updates); // HashMap<String, Integer>
// updated has pear -> 5, kept has pear -> 4, both have fig -> 2
On a LinkedHashMap, a key already present keeps its position and a new key goes to the end, as with put.
map, filter, partition and forEach take a function of the key and the value. mapValues, filterKeys and
similar methods work on one side. keySet() returns the keys as a set, and values() the values as a Vector, in
iteration order.
partition splits a map in one pass into two maps of its own kind: the entries that pass the test, and the others.
var stock = HashMap.of("apple", 3, "pear", 0, "fig", 7);
var split = stock.partition((fruit, count) -> count > 0);
// split._1() has apple and fig, split._2() has pear
Ranges¶
A TreeMap cuts out the entries whose keys lie between two bounds. The bounds need not be keys.
rangeFrom(from)keeps the keys at or afterfrom.rangeUntil(until)keeps those beforeuntil, andrangeTo(to)those at or beforeto.rangeFromUntil(from, until)keeps those at or afterfromand beforeuntil. It is empty whenfromis not beforeuntil; forfromafteruntil,java.util.TreeMap.subMapthrows instead.
Each result is a TreeMap with the same comparator, built in O(log n): it shares the rest of the tree with the map
it came from, and is that map itself when nothing is cut.
minAfter(key) is the entry with the least key at or after key, and maxBefore(key) the one with the greatest key
strictly before it, both as an Option of a Tuple2. iteratorFrom(start) walks the entries from start on.
var events = TreeMap.of(9, "standup", 12, "lunch", 15, "review", 18, "gym");
var workday = events.rangeFromUntil(9, 18); // TreeMap<Integer, String>
var next = events.minAfter(13); // Option<Tuple2<Integer, String>>
var last = events.maxBefore(12); // Option<Tuple2<Integer, String>>
// workday holds 9, 12 and 15, next is Some((15, review)), last is Some((9, standup))
Costs¶
| Operation | Cost | Note |
|---|---|---|
get |
effectively O(1) | effectively O(1): one hash lookup. |
containsKey |
effectively O(1) | effectively O(1): one hash lookup. |
put |
effectively O(1) | effectively O(1): one lookup and one put. |
putAll |
O(n + m) | O(n + m) for a HashMap of m entries (or the asJava view of one): the two maps are merged part by part, and a part only one of them holds is shared without being walked. For other entries, O(m): one effectively O(1) put each. O(1) when either map is empty. |
remove |
effectively O(1) | effectively O(1): one hash lookup, then a copy of a few small arrays; the rest is shared with this map. |
updateWith |
effectively O(1) | effectively O(1): one hash lookup, then at most one put or remove. |
keySet |
O(n) | O(n), with no hashing and no key comparison: the keys are copied from the trie of the entries, in the same shape. |
values |
O(n) | O(n): the values are copied into a new Vector. |
| Operation | Cost | Note |
|---|---|---|
get |
effectively O(1) | effectively O(1): one hash lookup. |
containsKey |
effectively O(1) | effectively O(1): one hash lookup. |
put |
effectively O(1) | effectively O(1): one lookup and one put. |
putAll |
O(m) | O(m) for m entries, one effectively O(1) put each. When this map is empty, the entries are built into a new map as ofEntries does, and a LinkedHashMap is returned as is. |
remove |
effectively O(1) | effectively O(1): one hash removal, and the key taken out of the insertion order on the path from its root, on any version of the map. |
updateWith |
effectively O(1) | effectively O(1): one hash lookup, then at most one put or one remove. |
keySet |
O(1) | O(1): a LinkedHashSet view that shares this map; nothing is copied. |
values |
O(n) | O(n): the values are copied into a new Vector. |
head |
effectively O(1) | effectively O(1): the first key of the insertion order, found from its root, then one hash lookup. |
take |
O(min(k, n - k)) | O(min(k, n - k)) for k kept entries: the smaller side, kept or dropped, is added to or taken out of a hash map, and the rest is shared; the insertion order is cut along the paths from its root to the cut. |
drop |
O(min(k, n - k)) | O(min(k, n - k)) for k dropped entries, as take. |
| Operation | Cost | Note |
|---|---|---|
get |
O(log n) | O(log n): one walk down the tree, comparing keys with the comparator. |
containsKey |
O(log n) | O(log n): one walk down the tree, comparing keys with the comparator. |
put |
O(log n) | O(log n): one lookup, then one insertion. |
putAll |
O(m log(n + m)) | O(m log(n + m)) for m entries, one insertion each. When entries is a TreeMap with an equal comparator (for a lambda, the same object), the cost is O(m log(n / m + 1)), m then being the smaller of the two sizes: the two trees are cut and joined, not rebuilt. O(1) when entries is an empty map. |
remove |
O(log n) | O(log n): one walk down the tree, then one deletion on the way back up when the key is present. |
updateWith |
O(log n) | O(log n): one lookup, then at most one insertion or deletion. |
keySet |
O(n) | O(n), with no comparison: the keys are copied from the tree of the entries, in the same shape. |
values |
O(n) | O(n): one walk in key order, the values copied into a Vector. |
head |
O(log n) | O(log n): the entry with the smallest key is found by walking down the tree, with no comparison. |
take |
O(log n) | O(log n): the tree is cut at that position without visiting the entries, and the result shares the rest of the tree. |
drop |
O(log n) | O(log n): the tree is cut at that position without visiting the entries, and the result shares the rest of the tree. |
Every method: complexity page.
When nothing changes¶
An operation that changes nothing returns the map itself, not a copy: no allocation, and == tells you nothing
changed.
putof the key and the value already there, the same objects. An equal key or value that is another object replaces the one there, so thatputreturns a new map.removeof an absent key,removeAllof absent keys, andmergewith keys this map already has.putAllof the map itself, of aHashMapwhose entries are all keys and values already there (one made from this map by removals, say), or of entries given one by one that are the keys and values already there.putAllof this map with one value replaced returns its argument, to which this map adds nothing.- The filters (
filter,filterKeys,filterValuesand theirrejectforms) andretainAllthat keep every entry;partitionreturns the map as the side that gets everything.retainAllkeeps the map's own keys and values, not the equal ones it is given. mapValuesandreplaceAllwhose function returns every value itself.replace(entry, newEntry)whose new key and value are the objects already there for the key ofentry. An equal key or value that is another object replaces the one there.takeanddropof aLinkedHashMaporTreeMap, and the ranges of aTreeMap, that cut nothing.putAllormergeon an empty map returns the argument when it is a map of the same kind.
When something changes, the parts of the tree or trie that do not are shared, not copied: putAll of a map of the
same kind shares the parts either map leaves as they are, and partition of a HashMap gives each side the parts
whose entries all go to it.
Equality¶
Two maps are equal when they hold the same entries, keys and values compared with their own equals. The kind does
not matter: a HashMap, a LinkedHashMap and a TreeMap of the same entries are all equal, whatever the key
comparator of the TreeMap. a.equals(b) and b.equals(a) always agree, and equal maps have the same hashCode.
A TreeMap's comparator decides the lookups: get, containsKey, put, remove and the bulk operations.
Equality is the one place where it does not. With a case-insensitive comparator, get("A") finds the value of the
key "a", and the map equals a map whose key is "a", not one whose key is "A".
Sharp edges¶
- Do not rely on the iteration order of a
HashMap: it depends on the hashes and may change between versions. asJava()on a map is ajava.util.Collectionof itsTuple2entries; thejava.util.Mapview isasJavaMap()(Java interop).- Neither keys nor values can be
null. A function or a supplier that returnsnullfails with aNullPointerExceptionnaming the method. - With two
getOrElseoverloads, anulldefault needs a cast to the value type:getOrElse(key, (Integer) null). Without it,getOrElse(key, null)does not compile on most maps, and on aMap<K, Object>it picks theSupplieroverload and throwssupplier is null, even when the key is present. On aMap<K, Object>, aSupplierpassed as the default is called, not returned. groupMapgroups what its function returns for each entry in aVector, in the map's iteration order, asvalues()does;groupBygroups whole entries in maps of the same type.