Java maps
Java's Map implementations and idioms: HashMap, LinkedHashMap, TreeMap, getOrDefault, computeIfAbsent, merge, iteration order and null keys.
Learning goals
- Choose between HashMap, LinkedHashMap and TreeMap
- Write counting and grouping code with getOrDefault, merge and computeIfAbsent
- Predict iteration order and null-key behaviour
Prerequisites
HashMap
HashMap<K, V> is the default map: separate chaining over a power-of-two table, load factor 0.75, expected O(1) put, get, containsKey and remove. Iteration order is unspecified. put returns the previous value or null, get returns null when the key is absent, and a key is identified by hashCode + equals.
It is not thread-safe; use ConcurrentHashMap when several threads write.
1static void basics() {2Map<String, Integer> age = new HashMap<>();3age.put("Ada", 36);4Integer old = age.put("Ada", 37); // returns the previous value: 365System.out.println(old + " " + age.get("Ada") + " " + age.get("Bob") + " " + age.containsKey("Bob"));6}
LinkedHashMap
LinkedHashMap is a HashMap plus a doubly linked list through its entries, so iteration follows **insertion order** (or, with accessOrder = true, order of last access). The cost is a little extra memory; lookups stay O(1).
Access order plus removeEldestEntry gives an LRU cache in a few lines: each get moves the entry to the end, and the oldest entry (the least recently used) is dropped when the size passes the capacity. After putting 1, 2, 3, reading 1 and putting 4 into a capacity-3 cache the keys are [3, 1, 4].
1static <K, V> Map<K, V> lruCache(int capacity) {2return new LinkedHashMap<K, V>(16, 0.75f, true) { // true = access order3@Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {4return size() > capacity; // drop the least recently used5}6};7}
TreeMap comparison
TreeMap keeps keys **sorted** in a red-black tree: put, get and remove are O(log n), but it offers operations a hash map cannot: firstKey, lastKey, floorKey, ceilingKey, headMap, tailMap and subMap. It orders keys with compareTo or a Comparator, never with hashCode or equals.
Choose by the question you need to answer: exact lookup by key (HashMap), lookup plus insertion order (LinkedHashMap), or order-based queries such as “the largest key not above x” (TreeMap).
| HashMap | LinkedHashMap | TreeMap | |
|---|---|---|---|
| Lookup / insert / remove | O(1) expected | O(1) expected | O(log n) |
| Iteration order | Unspecified | Insertion (or access) order | Sorted by key |
| Range / floor / ceiling queries | No | No | Yes |
| Null key | One allowed | One allowed | Not allowed (natural ordering) |
| Key requirement | hashCode + equals | hashCode + equals | Comparable or a Comparator |
1static void navigate() {2TreeMap<Integer, String> tm = new TreeMap<>(Map.of(10, "a", 20, "b", 30, "c"));3System.out.println(tm.floorKey(25) + " " + tm.ceilingKey(25) + " " + tm.firstKey() + " " + tm.headMap(20));4// prints: 20 30 10 {10=a}5}
getOrDefault
getOrDefault(key, d) returns the value for key, or d if the key is absent. It does **not** insert the default, so the usual counting idiom needs a put: counts.put(w, counts.getOrDefault(w, 0) + 1). That is two hash lookups per word.
One trap: if the key is present with a stored null value, getOrDefault returns null, not the default, because the key exists. Choose the “Count with getOrDefault” option in the animation and watch the map being updated word by word. HashMap order is unspecified, so the animation lists entries in the order they were first inserted; the real map for “to be or not to be” prints {not=1, or=1, be=2, to=2}.
1static Map<String, Integer> countWords(String[] words) {2Map<String, Integer> counts = new HashMap<>();3for (String w : words) counts.put(w, counts.getOrDefault(w, 0) + 1); // a missing key counts as 04return counts;5}
getOrDefault, merge and computeIfAbsent
Interactive · edit the inputwords
map (shown in first-insertion order; HashMap order is not guaranteed)
An empty map from a word to how many times it has appeared.
1Map<String, Integer> counts = new HashMap<>();2for (String w : words) counts.put(w, counts.getOrDefault(w, 0) + 1); // option 0: a default for a missing key3for (String w : words) counts.merge(w, 1, Integer::sum); // option 1: insert 1, or combine with the old value4Map<Integer, List<String>> byLength = new HashMap<>();5for (String w : words) byLength.computeIfAbsent(w.length(), k -> new ArrayList<>()).add(w); // option 2: create the list on first use
Live state
words
6
Execution trace (dry run table)Show 14 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | words=6 | An empty map from a word to how many times it has appeared. |
| 2 | L2 | w=to getOrDefault=0 | getOrDefault("to", 0): the key is missing, so it returns the default 0. |
| 3 | L2 | w=to count=1 | put("to", 0 + 1): two map operations (a get and a put) for one update. |
| 4 | L2 | w=be getOrDefault=0 | getOrDefault("be", 0): the key is missing, so it returns the default 0. |
| 5 | L2 | w=be count=1 | put("be", 0 + 1): two map operations (a get and a put) for one update. |
| 6 | L2 | w=or getOrDefault=0 | getOrDefault("or", 0): the key is missing, so it returns the default 0. |
| 7 | L2 | w=or count=1 | put("or", 0 + 1): two map operations (a get and a put) for one update. |
| 8 | L2 | w=not getOrDefault=0 | getOrDefault("not", 0): the key is missing, so it returns the default 0. |
| 9 | L2 | w=not count=1 | put("not", 0 + 1): two map operations (a get and a put) for one update. |
| 10 | L2 | w=to getOrDefault=1 | getOrDefault("to", 0) returns the stored 1. |
| 11 | L2 | w=to count=2 | put("to", 1 + 1): two map operations (a get and a put) for one update. |
| 12 | L2 | w=be getOrDefault=1 | getOrDefault("be", 0) returns the stored 1. |
| 13 | L2 | w=be count=2 | put("be", 1 + 1): two map operations (a get and a put) for one update. |
| 14 | L2 | return={to=2, be=2, or=1, not=1} | Final map: {to=2, be=2, or=1, not=1}. |
computeIfAbsent
computeIfAbsent(key, f) returns the existing value, or calls f to create one, stores it and returns it. It is the standard way to build a map of lists: map.computeIfAbsent(k, x -> new ArrayList<>()).add(v) replaces the three-step “get, check for null, put”. Choose “Group by length with computeIfAbsent” in the animation.
The mapping function must not modify the same map: HashMap detects this and throws ConcurrentModificationException (Java 9 and later). Also, computeIfAbsent treats a key mapped to null as absent and replaces it.
1static Map<Integer, List<String>> groupByLength(String[] words) {2Map<Integer, List<String>> byLength = new HashMap<>();3for (String w : words) byLength.computeIfAbsent(w.length(), k -> new ArrayList<>()).add(w);4return byLength; // {2=[to, be, or, to, be], 3=[not]} for "to be or not to be"5}
getOrDefault, merge and computeIfAbsent
Interactive · edit the inputwords
map (shown in first-insertion order; HashMap order is not guaranteed)
An empty map from a word to how many times it has appeared.
1Map<String, Integer> counts = new HashMap<>();2for (String w : words) counts.put(w, counts.getOrDefault(w, 0) + 1); // option 0: a default for a missing key3for (String w : words) counts.merge(w, 1, Integer::sum); // option 1: insert 1, or combine with the old value4Map<Integer, List<String>> byLength = new HashMap<>();5for (String w : words) byLength.computeIfAbsent(w.length(), k -> new ArrayList<>()).add(w); // option 2: create the list on first use
Live state
words
6
Execution trace (dry run table)Show 14 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | words=6 | An empty map from a word to how many times it has appeared. |
| 2 | L2 | w=to getOrDefault=0 | getOrDefault("to", 0): the key is missing, so it returns the default 0. |
| 3 | L2 | w=to count=1 | put("to", 0 + 1): two map operations (a get and a put) for one update. |
| 4 | L2 | w=be getOrDefault=0 | getOrDefault("be", 0): the key is missing, so it returns the default 0. |
| 5 | L2 | w=be count=1 | put("be", 0 + 1): two map operations (a get and a put) for one update. |
| 6 | L2 | w=or getOrDefault=0 | getOrDefault("or", 0): the key is missing, so it returns the default 0. |
| 7 | L2 | w=or count=1 | put("or", 0 + 1): two map operations (a get and a put) for one update. |
| 8 | L2 | w=not getOrDefault=0 | getOrDefault("not", 0): the key is missing, so it returns the default 0. |
| 9 | L2 | w=not count=1 | put("not", 0 + 1): two map operations (a get and a put) for one update. |
| 10 | L2 | w=to getOrDefault=1 | getOrDefault("to", 0) returns the stored 1. |
| 11 | L2 | w=to count=2 | put("to", 1 + 1): two map operations (a get and a put) for one update. |
| 12 | L2 | w=be getOrDefault=1 | getOrDefault("be", 0) returns the stored 1. |
| 13 | L2 | w=be count=2 | put("be", 1 + 1): two map operations (a get and a put) for one update. |
| 14 | L2 | return={to=2, be=2, or=1, not=1} | Final map: {to=2, be=2, or=1, not=1}. |
merge
merge(key, value, f) does the whole read-modify-write in one call: if the key is absent it stores value; otherwise it stores f(oldValue, value). If f returns null the key is **removed**. Counting becomes a one-liner, counts.merge(w, 1, Integer::sum), with a single lookup. Choose “Count with merge”.
Use merge for counting, summing and collecting; use computeIfAbsent when you need to create a mutable container; use getOrDefault for read-only access with a fallback.
1static void mergeDemo() {2Map<String, Integer> c = new HashMap<>();3c.merge("x", 1, Integer::sum);4c.merge("x", 1, Integer::sum); // {x=2}5c.merge("x", 5, (oldV, newV) -> null); // the function returned null: key removed6System.out.println(c); // {}7}
getOrDefault, merge and computeIfAbsent
Interactive · edit the inputwords
map (shown in first-insertion order; HashMap order is not guaranteed)
An empty map from a word to how many times it has appeared.
1Map<String, Integer> counts = new HashMap<>();2for (String w : words) counts.put(w, counts.getOrDefault(w, 0) + 1); // option 0: a default for a missing key3for (String w : words) counts.merge(w, 1, Integer::sum); // option 1: insert 1, or combine with the old value4Map<Integer, List<String>> byLength = new HashMap<>();5for (String w : words) byLength.computeIfAbsent(w.length(), k -> new ArrayList<>()).add(w); // option 2: create the list on first use
Live state
words
6
Execution trace (dry run table)Show 14 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | words=6 | An empty map from a word to how many times it has appeared. |
| 2 | L2 | w=to getOrDefault=0 | getOrDefault("to", 0): the key is missing, so it returns the default 0. |
| 3 | L2 | w=to count=1 | put("to", 0 + 1): two map operations (a get and a put) for one update. |
| 4 | L2 | w=be getOrDefault=0 | getOrDefault("be", 0): the key is missing, so it returns the default 0. |
| 5 | L2 | w=be count=1 | put("be", 0 + 1): two map operations (a get and a put) for one update. |
| 6 | L2 | w=or getOrDefault=0 | getOrDefault("or", 0): the key is missing, so it returns the default 0. |
| 7 | L2 | w=or count=1 | put("or", 0 + 1): two map operations (a get and a put) for one update. |
| 8 | L2 | w=not getOrDefault=0 | getOrDefault("not", 0): the key is missing, so it returns the default 0. |
| 9 | L2 | w=not count=1 | put("not", 0 + 1): two map operations (a get and a put) for one update. |
| 10 | L2 | w=to getOrDefault=1 | getOrDefault("to", 0) returns the stored 1. |
| 11 | L2 | w=to count=2 | put("to", 1 + 1): two map operations (a get and a put) for one update. |
| 12 | L2 | w=be getOrDefault=1 | getOrDefault("be", 0) returns the stored 1. |
| 13 | L2 | w=be count=2 | put("be", 1 + 1): two map operations (a get and a put) for one update. |
| 14 | L2 | return={to=2, be=2, or=1, not=1} | Final map: {to=2, be=2, or=1, not=1}. |
Iteration order
HashMap and HashSet iterate over the bucket array from index 0 upwards, and within a bucket in chain order. For small Integer keys the bucket is the value itself modulo the capacity, so keys often appear sorted by accident; for other keys the order looks random and can change when the table resizes. Never write code that depends on it.
When order matters, say so with the type: LinkedHashMap/LinkedHashSet for insertion order, TreeMap/TreeSet for sorted order. The animation shows the same values in all collections: try “Colliding buckets (mod 16)”.
| Values added: 42, 7, 19, 7, 3, 25, 16, 33 | Result |
|---|---|
| HashSet | [16, 33, 19, 3, 7, 25, 42] |
| LinkedHashSet | [42, 7, 19, 3, 25, 16, 33] |
| TreeSet | [3, 7, 16, 19, 25, 33, 42] |
Same values, different collections
Interactive · edit the inputArrayList
LinkedHashSet
HashSet (capacity 16)
TreeSet
PriorityQueue array
Five collections receive exactly the same values in the same order. Watch how each one stores them.
1List<Integer> list = new ArrayList<>();2Set<Integer> linked = new LinkedHashSet<>();3Set<Integer> hash = new HashSet<>();4Set<Integer> tree = new TreeSet<>();5PriorityQueue<Integer> heap = new PriorityQueue<>();6for (int x : arr) {7list.add(x); // keeps insertion order, keeps duplicates8linked.add(x); // keeps insertion order, drops duplicates9hash.add(x); // order decided by hash buckets10tree.add(x); // always sorted11heap.add(x); // heap array: only the smallest is guaranteed first12}13System.out.println(list + " " + linked + " " + hash + " " + tree + " " + heap);
Live state
values
8
Execution trace (dry run table)Show 10 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | values=8 | Five collections receive exactly the same values in the same order. Watch how each one stores them. |
| 2 | L7 | x=42 adds so far=1 distinct=1 HashSet capacity=16 | 42 goes to bucket 10 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15). |
| 3 | L7 | x=7 adds so far=2 distinct=2 HashSet capacity=16 | 7 goes to bucket 7 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15). |
| 4 | L7 | x=19 adds so far=3 distinct=3 HashSet capacity=16 | 19 goes to bucket 3 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15). |
| 5 | L7 | x=7 adds so far=4 distinct=3 HashSet capacity=16 | 7 is already present: the ArrayList and the PriorityQueue keep a second copy, the three sets ignore it. |
| 6 | L7 | x=3 adds so far=5 distinct=4 HashSet capacity=16 | 3 goes to bucket 3 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15). |
| 7 | L7 | x=25 adds so far=6 distinct=5 HashSet capacity=16 | 25 goes to bucket 9 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15). |
| 8 | L7 | x=16 adds so far=7 distinct=6 HashSet capacity=16 | 16 goes to bucket 0 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15). |
| 9 | L7 | x=33 adds so far=8 distinct=7 HashSet capacity=16 | 33 goes to bucket 1 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15). |
| 10 | L13 | distinct=7 capacity=16 | Same values, five different orders. Only the TreeSet is sorted; the HashSet order is an implementation detail you must never depend on, and the PriorityQueue promises only that index 0 is the minimum. |
Null keys
HashMap allows one null key (it hashes to 0, so it lives in bucket 0) and any number of null values. TreeMap with natural ordering throws NullPointerException for a null key because it cannot compare it. ConcurrentHashMap, Hashtable and the immutable Map.of(...) reject null keys and values (Map.of(...).get(null) also throws).
Null values make get ambiguous: a result of null can mean absent or present-with-null. Use containsKey to tell them apart, or avoid null values altogether.
1static void nulls() {2Map<String, Integer> hm = new HashMap<>();3hm.put(null, 1); hm.put("a", 2);4System.out.println(hm.get(null)); // 15try { new TreeMap<String, Integer>().put(null, 1); }6catch (NullPointerException e) { System.out.println("TreeMap: no null key"); }7}
Practice ladder
Now try it yourself
Easy
- Two Sum
- Word Pattern
Medium
- Group Anagrams
- Top K Frequent Elements
Advanced
- LRU Cache
- Design a time-based key-value store
Key takeaways
- HashMap for lookup, LinkedHashMap for insertion or access order, TreeMap for sorted order and range queries.
- merge counts with one lookup, computeIfAbsent builds maps of lists, getOrDefault reads with a fallback.
- Never depend on HashMap iteration order; null keys work in HashMap but not in TreeMap or ConcurrentHashMap.