Chapter 14: Hash Tables, Maps and Sets
14.04Beginner / Intermediate8 topics20 min4 interactive animations

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

Hashing basicsHash table mechanics
14.04.1

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.

Java
1static void basics() {
2 Map<String, Integer> age = new HashMap<>();
3 age.put("Ada", 36);
4 Integer old = age.put("Ada", 37); // returns the previous value: 36
5 System.out.println(old + " " + age.get("Ada") + " " + age.get("Bob") + " " + age.containsKey("Bob"));
6}
14.04.2

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].

Java
1static <K, V> Map<K, V> lruCache(int capacity) {
2 return new LinkedHashMap<K, V>(16, 0.75f, true) { // true = access order
3 @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
4 return size() > capacity; // drop the least recently used
5 }
6 };
7}
14.04.3

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).

HashMapLinkedHashMapTreeMap
Lookup / insert / removeO(1) expectedO(1) expectedO(log n)
Iteration orderUnspecifiedInsertion (or access) orderSorted by key
Range / floor / ceiling queriesNoNoYes
Null keyOne allowedOne allowedNot allowed (natural ordering)
Key requirementhashCode + equalshashCode + equalsComparable or a Comparator
Java
1static void navigate() {
2 TreeMap<Integer, String> tm = new TreeMap<>(Map.of(10, "a", 20, "b", 30, "c"));
3 System.out.println(tm.floorKey(25) + " " + tm.ceilingKey(25) + " " + tm.firstKey() + " " + tm.headMap(20));
4 // prints: 20 30 10 {10=a}
5}
14.04.4

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}.

Java
1static Map<String, Integer> countWords(String[] words) {
2 Map<String, Integer> counts = new HashMap<>();
3 for (String w : words) counts.put(w, counts.getOrDefault(w, 0) + 1); // a missing key counts as 0
4 return counts;
5}

getOrDefault, merge and computeIfAbsent

Interactive · edit the input

Your input

Try

words

to0
be1
or2
not3
to4
be5

map (shown in first-insertion order; HashMap order is not guaranteed)

∅
Setup

An empty map from a word to how many times it has appeared.

Step 1 / 14
Java
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 key
3for (String w : words) counts.merge(w, 1, Integer::sum); // option 1: insert 1, or combine with the old value
4Map<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

No condition evaluated on this step
Execution trace (dry run table)Show 14 steps
StepLineStateWhat happens
1L1words=6An empty map from a word to how many times it has appeared.
2L2w=to getOrDefault=0getOrDefault("to", 0): the key is missing, so it returns the default 0.
3L2w=to count=1put("to", 0 + 1): two map operations (a get and a put) for one update.
4L2w=be getOrDefault=0getOrDefault("be", 0): the key is missing, so it returns the default 0.
5L2w=be count=1put("be", 0 + 1): two map operations (a get and a put) for one update.
6L2w=or getOrDefault=0getOrDefault("or", 0): the key is missing, so it returns the default 0.
7L2w=or count=1put("or", 0 + 1): two map operations (a get and a put) for one update.
8L2w=not getOrDefault=0getOrDefault("not", 0): the key is missing, so it returns the default 0.
9L2w=not count=1put("not", 0 + 1): two map operations (a get and a put) for one update.
10L2w=to getOrDefault=1getOrDefault("to", 0) returns the stored 1.
11L2w=to count=2put("to", 1 + 1): two map operations (a get and a put) for one update.
12L2w=be getOrDefault=1getOrDefault("be", 0) returns the stored 1.
13L2w=be count=2put("be", 1 + 1): two map operations (a get and a put) for one update.
14L2return={to=2, be=2, or=1, not=1}Final map: {to=2, be=2, or=1, not=1}.
14.04.5

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.

Java
1static Map<Integer, List<String>> groupByLength(String[] words) {
2 Map<Integer, List<String>> byLength = new HashMap<>();
3 for (String w : words) byLength.computeIfAbsent(w.length(), k -> new ArrayList<>()).add(w);
4 return byLength; // {2=[to, be, or, to, be], 3=[not]} for "to be or not to be"
5}

getOrDefault, merge and computeIfAbsent

Interactive · edit the input

Your input

Try

words

to0
be1
or2
not3
to4
be5

map (shown in first-insertion order; HashMap order is not guaranteed)

∅
Setup

An empty map from a word to how many times it has appeared.

Step 1 / 14
Java
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 key
3for (String w : words) counts.merge(w, 1, Integer::sum); // option 1: insert 1, or combine with the old value
4Map<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

No condition evaluated on this step
Execution trace (dry run table)Show 14 steps
StepLineStateWhat happens
1L1words=6An empty map from a word to how many times it has appeared.
2L2w=to getOrDefault=0getOrDefault("to", 0): the key is missing, so it returns the default 0.
3L2w=to count=1put("to", 0 + 1): two map operations (a get and a put) for one update.
4L2w=be getOrDefault=0getOrDefault("be", 0): the key is missing, so it returns the default 0.
5L2w=be count=1put("be", 0 + 1): two map operations (a get and a put) for one update.
6L2w=or getOrDefault=0getOrDefault("or", 0): the key is missing, so it returns the default 0.
7L2w=or count=1put("or", 0 + 1): two map operations (a get and a put) for one update.
8L2w=not getOrDefault=0getOrDefault("not", 0): the key is missing, so it returns the default 0.
9L2w=not count=1put("not", 0 + 1): two map operations (a get and a put) for one update.
10L2w=to getOrDefault=1getOrDefault("to", 0) returns the stored 1.
11L2w=to count=2put("to", 1 + 1): two map operations (a get and a put) for one update.
12L2w=be getOrDefault=1getOrDefault("be", 0) returns the stored 1.
13L2w=be count=2put("be", 1 + 1): two map operations (a get and a put) for one update.
14L2return={to=2, be=2, or=1, not=1}Final map: {to=2, be=2, or=1, not=1}.
14.04.6

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.

Java
1static void mergeDemo() {
2 Map<String, Integer> c = new HashMap<>();
3 c.merge("x", 1, Integer::sum);
4 c.merge("x", 1, Integer::sum); // {x=2}
5 c.merge("x", 5, (oldV, newV) -> null); // the function returned null: key removed
6 System.out.println(c); // {}
7}

getOrDefault, merge and computeIfAbsent

Interactive · edit the input

Your input

Try

words

to0
be1
or2
not3
to4
be5

map (shown in first-insertion order; HashMap order is not guaranteed)

∅
Setup

An empty map from a word to how many times it has appeared.

Step 1 / 14
Java
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 key
3for (String w : words) counts.merge(w, 1, Integer::sum); // option 1: insert 1, or combine with the old value
4Map<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

No condition evaluated on this step
Execution trace (dry run table)Show 14 steps
StepLineStateWhat happens
1L1words=6An empty map from a word to how many times it has appeared.
2L2w=to getOrDefault=0getOrDefault("to", 0): the key is missing, so it returns the default 0.
3L2w=to count=1put("to", 0 + 1): two map operations (a get and a put) for one update.
4L2w=be getOrDefault=0getOrDefault("be", 0): the key is missing, so it returns the default 0.
5L2w=be count=1put("be", 0 + 1): two map operations (a get and a put) for one update.
6L2w=or getOrDefault=0getOrDefault("or", 0): the key is missing, so it returns the default 0.
7L2w=or count=1put("or", 0 + 1): two map operations (a get and a put) for one update.
8L2w=not getOrDefault=0getOrDefault("not", 0): the key is missing, so it returns the default 0.
9L2w=not count=1put("not", 0 + 1): two map operations (a get and a put) for one update.
10L2w=to getOrDefault=1getOrDefault("to", 0) returns the stored 1.
11L2w=to count=2put("to", 1 + 1): two map operations (a get and a put) for one update.
12L2w=be getOrDefault=1getOrDefault("be", 0) returns the stored 1.
13L2w=be count=2put("be", 1 + 1): two map operations (a get and a put) for one update.
14L2return={to=2, be=2, or=1, not=1}Final map: {to=2, be=2, or=1, not=1}.
14.04.7

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, 33Result
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 input

Your input

Try

ArrayList

empty

LinkedHashSet

empty

HashSet (capacity 16)

empty

TreeSet

empty

PriorityQueue array

empty
Setup

Five collections receive exactly the same values in the same order. Watch how each one stores them.

Step 1 / 10
Java
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) {
7 list.add(x); // keeps insertion order, keeps duplicates
8 linked.add(x); // keeps insertion order, drops duplicates
9 hash.add(x); // order decided by hash buckets
10 tree.add(x); // always sorted
11 heap.add(x); // heap array: only the smallest is guaranteed first
12}
13System.out.println(list + " " + linked + " " + hash + " " + tree + " " + heap);

Live state

values

8

No condition evaluated on this step
Execution trace (dry run table)Show 10 steps
StepLineStateWhat happens
1L1values=8Five collections receive exactly the same values in the same order. Watch how each one stores them.
2L7x=42 adds so far=1 distinct=1 HashSet capacity=1642 goes to bucket 10 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15).
3L7x=7 adds so far=2 distinct=2 HashSet capacity=167 goes to bucket 7 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15).
4L7x=19 adds so far=3 distinct=3 HashSet capacity=1619 goes to bucket 3 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15).
5L7x=7 adds so far=4 distinct=3 HashSet capacity=167 is already present: the ArrayList and the PriorityQueue keep a second copy, the three sets ignore it.
6L7x=3 adds so far=5 distinct=4 HashSet capacity=163 goes to bucket 3 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15).
7L7x=25 adds so far=6 distinct=5 HashSet capacity=1625 goes to bucket 9 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15).
8L7x=16 adds so far=7 distinct=6 HashSet capacity=1616 goes to bucket 0 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15).
9L7x=33 adds so far=8 distinct=7 HashSet capacity=1633 goes to bucket 1 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15).
10L13distinct=7 capacity=16Same 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.
14.04.8

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.

Java
1static void nulls() {
2 Map<String, Integer> hm = new HashMap<>();
3 hm.put(null, 1); hm.put("a", 2);
4 System.out.println(hm.get(null)); // 1
5 try { new TreeMap<String, Integer>().put(null, 1); }
6 catch (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.