AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

14 Collections framework / Lesson 4 of 8

Maps — HashMap internals, TreeMap, LinkedHashMap and the modern API

16 min · +10 XP · 2 exercises · 5 questions

Read the text16 min
Exercises0/2
Quiz5 q

Quick answer

How does HashMap work internally in Java?

A Java HashMap stores entries in an array of buckets, 16 by default. To put a key it spreads the key's hashCode, picks a bucket from it, then walks that bucket comparing hashes and equals, replacing or appending a node. When the size passes 0.75 of the capacity the table doubles, and since Java 8 a bucket chain longer than eight becomes a red-black tree.

Map<K, V> is the most used data structure after List, and HashMap is the one every interviewer asks about: how it stores entries, what happens on collision, why it resizes, why hashCode matters. This lesson covers that machinery, the three main implementations, and the Java 8 methods — getOrDefault, merge, computeIfAbsent, putIfAbsent — that replaced most of the null-checking boilerplate maps used to require.

HashMap internals

A HashMap is an array of buckets (Node<K,V>[] table, length a power of two, default 16). To put(key, value):

  1. Compute h = key.hashCode(), spread its high bits into the low bits (h ^ (h >>> 16)), and take the bucket index h & (table.length - 1).
  2. If the bucket is empty, store a new node there.
  3. Otherwise walk the bucket's chain comparing hash first and then equals; replace the value if the key is found, else append a node.
  4. If size exceeds capacity × loadFactor (0.75 by default), resize: double the table and redistribute every node.

get(key) is the same hash-and-walk, without the insert. With well-distributed hash codes each bucket holds about one node and both are O(1). When many keys share a bucket (bad hashCode, or a deliberate collision attack), a chain longer than 8 is converted into a red-black tree (Java 8+), bounding the worst case at O(log n) — provided the keys are Comparable.

Consequences you must know:

  • hashCode and equals must agree (Module 8), or get looks in the wrong bucket.
  • Keys must not change while in the map — the bucket was chosen from the old hash.
  • Iteration order is unspecified and changes after a resize. Never rely on it.
  • Initial capacity (new HashMap<>(expectedSize / 0.75 + 1)) avoids resizes when the count is known; the default is fine otherwise.
  • One null key is allowed (bucket 0); null values are allowed — which is why get returning null is ambiguous (containsKey disambiguates).

The three implementations

HashMapLinkedHashMapTreeMap
get/put/removeO(1) avgO(1) avgO(log n)
Ordernoneinsertion (or access, with a flag)sorted by key
Null keysoneonenone
Needsequals/hashCode on keyssameComparable keys or a Comparator
Extra—removeEldestEntry → LRU cachefirstKey, floorKey, headMap, subMap, descendingMap

LinkedHashMap threads a doubly linked list through the entries; with accessOrder = true every get moves the entry to the end, and overriding removeEldestEntry gives a bounded LRU cache in five lines (Module 13's exercise). TreeMap is a red-black tree keyed by compareTo — the navigation methods are its reason to exist.

The modern API

Map<String, Integer> counts = new HashMap<>();

// counting — the old way, with a null check
Integer c = counts.get(word);
counts.put(word, c == null ? 1 : c + 1);

// counting — the modern way
counts.merge(word, 1, Integer::sum);                     // put 1 if absent, else combine old and 1

// reading with a default
int n = counts.getOrDefault(word, 0);                    // no null, no unboxing NPE

// multimap: a list per key, created on first use
Map<String, List<String>> byInitial = new HashMap<>();
byInitial.computeIfAbsent(word.substring(0, 1), k -> new ArrayList<>()).add(word);

// insert only if absent
counts.putIfAbsent("x", 0);                              // returns the existing value or null

// recompute in place
counts.compute("x", (k, v) -> v == null ? 1 : v * 2);
counts.computeIfPresent("x", (k, v) -> v > 100 ? null : v);   // returning null REMOVES the entry

// bulk
counts.forEach((k, v) -> System.out.println(k + "=" + v));
counts.replaceAll((k, v) -> v * 10);
counts.entrySet().removeIf(e -> e.getValue() == 0);

merge(key, value, fn): if absent, store value; else store fn(old, value); a null result removes the key. computeIfAbsent(key, fn): if absent (or mapped to null), store and return fn(key); else return the existing value — and the mapping function must not modify the map. These two replace the majority of if (map.containsKey(...)) code.

Iterating

for (Map.Entry<String, Integer> e : counts.entrySet()) {     // the efficient way: one lookup per entry
    e.getKey(); e.getValue(); e.setValue(e.getValue() + 1);    // setValue writes through
}
for (String k : counts.keySet()) counts.get(k);              // two lookups per entry — avoid
for (int v : counts.values()) …
counts.forEach((k, v) -> …);

keySet(), values() and entrySet() are views: removing from them removes from the map (counts.keySet().removeIf(...), counts.values().remove(0) removes one entry with value 0), and adding to them is unsupported. Modifying the map while iterating a view throws ConcurrentModificationException — except through the iterator's own remove or entry.setValue.

TreeMap navigation

TreeMap<Integer, String> t = new TreeMap<>();
t.firstKey(); t.lastKey(); t.firstEntry();
t.floorKey(25); t.ceilingKey(25); t.lowerKey(20); t.higherKey(20);
t.headMap(30); t.tailMap(30, true); t.subMap(10, 30);       // views
t.pollFirstEntry(); t.descendingMap();

Same navigation as TreeSet, with entries. Interval lookups, "the price valid at date d" (floorEntry(d)), leaderboards and ordered reports are TreeMap jobs.

Choosing a key type

Keys must be immutable with correct equals/hashCode (hash maps) or a consistent compareTo (tree maps): String, Integer, Long, enums, records, UUID, LocalDate. Composite keys are records (record Cell(int r, int c)). Never arrays (identity equality), never mutable objects.

Map.of, Map.entry, copies

Map.of("a", 1, "b", 2) (up to 10 pairs) and Map.ofEntries(Map.entry("a", 1), …) are immutable, null-hostile, order-randomised. Map.copyOf copies; Collections.unmodifiableMap wraps. new HashMap<>(other) copies into a mutable map.

Interview angle

  • "How does HashMap work?" Hash → bucket index; chain (then tree) of nodes; equals to confirm; resize at 0.75 load; O(1) average.
  • "What happens with a bad hashCode?" Everything lands in one bucket: O(n) lookups, or O(log n) once treeified.
  • "Why is 0.75 the load factor?" A time/space compromise: fewer collisions than 1.0, less waste than 0.5.
  • "HashMap vs Hashtable vs ConcurrentHashMap?" Unsynchronised / legacy fully synchronised / modern concurrent with striped locking (Module 17).
  • "How do you count words?" merge(word, 1, Integer::sum).
  • "Can a HashMap have a null key?" One. TreeMap and ConcurrentHashMap cannot.

Key takeaways

  • HashMap: buckets by spread hashCode, chains that treeify, resize at 0.75 — O(1) with good hashes; immutable keys with consistent equals/hashCode.
  • LinkedHashMap for order and LRU; TreeMap for sorted keys and floorKey-style navigation; none of the three is thread-safe.
  • getOrDefault, merge, computeIfAbsent, putIfAbsent, compute replace null-check boilerplate; a null result from compute*/merge removes.
  • Iterate entrySet(); the views write through and are fail-fast.

Common questions

What happens when two keys have the same hash in a HashMap?

They collide in one bucket, and the map keeps both nodes in that bucket's chain, telling them apart with equals. Lookups walk the chain, so many collisions degrade towards O(n); since Java 8 a chain longer than eight is converted to a red-black tree, bounding it at O(log n) for Comparable keys.

What is the difference between HashMap, LinkedHashMap and TreeMap?

HashMap is O(1) on average with no iteration order. LinkedHashMap adds insertion order, or access order with a flag, which together with removeEldestEntry makes an LRU cache. TreeMap keeps keys sorted in a red-black tree at O(log n), with navigation such as floorKey and headMap.

Can a HashMap have a null key?

Yes, one. HashMap allows a single null key and any number of null values, which makes get returning null ambiguous — use containsKey to tell an absent key from a null value. TreeMap and ConcurrentHashMap do not allow null keys.

What does computeIfAbsent do in Java?

computeIfAbsent(key, fn) returns the value for key, first storing fn(key) if the key is absent or mapped to null. It is the multimap idiom: map.computeIfAbsent(k, x -> new ArrayList<>()).add(v) creates each list on first use. For counting, merge(word, 1, Integer::sum) does the same job.

Why is the HashMap load factor 0.75?

It is a compromise between time and space: a load factor of 1.0 would mean more collisions, and 0.5 would waste more of the table. When the number of entries exceeds capacity times 0.75, the table doubles and every entry is redistributed.

Mark as readSign in to keep your place.

How HashMap Works Internally in Java: Buckets and Resizing

Java HashMap hashes each key to a bucket, checks equals, treeifies long chains and resizes at 0.75 load. Plus TreeMap, LinkedHashMap, merge and computeIfAbsent.

  • Course: Java study plan
  • Module: Collections framework
  • Kind: Lesson
  • Reading time: 16 min
  • Runtime: OpenJDK 18

How does HashMap work internally in Java?

A Java HashMap stores entries in an array of buckets, 16 by default. To put a key it spreads the key's hashCode, picks a bucket from it, then walks that bucket comparing hashes and equals, replacing or appending a node. When the size passes 0.75 of the capacity the table doubles, and since Java 8 a bucket chain longer than eight becomes a red-black tree.

Lesson

Map<K, V> is the most used data structure after List, and HashMap is the one every interviewer asks about: how it stores entries, what happens on collision, why it resizes, why hashCode matters. This lesson covers that machinery, the three main implementations, and the Java 8 methods — getOrDefault, merge, computeIfAbsent, putIfAbsent — that replaced most of the null-checking boilerplate maps used to require.

HashMap internals

A HashMap is an array of buckets (Node<K,V>[] table, length a power of two, default 16). To put(key, value):

  1. Compute h = key.hashCode(), spread its high bits into the low bits (h ^ (h >>> 16)), and take the bucket index h & (table.length - 1).
  2. If the bucket is empty, store a new node there.
  3. Otherwise walk the bucket's chain comparing hash first and then equals; replace the value if the key is found, else append a node.
  4. If size exceeds capacity × loadFactor (0.75 by default), resize: double the table and redistribute every node.

get(key) is the same hash-and-walk, without the insert. With well-distributed hash codes each bucket holds about one node and both are O(1). When many keys share a bucket (bad hashCode, or a deliberate collision attack), a chain longer than 8 is converted into a red-black tree (Java 8+), bounding the worst case at O(log n) — provided the keys are Comparable.

Consequences you must know:

  • hashCode and equals must agree (Module 8), or get looks in the wrong bucket.
  • Keys must not change while in the map — the bucket was chosen from the old hash.
  • Iteration order is unspecified and changes after a resize. Never rely on it.
  • Initial capacity (new HashMap<>(expectedSize / 0.75 + 1)) avoids resizes when the count is known; the default is fine otherwise.
  • One null key is allowed (bucket 0); null values are allowed — which is why get returning null is ambiguous (containsKey disambiguates).

The three implementations

HashMapLinkedHashMapTreeMap
get/put/removeO(1) avgO(1) avgO(log n)
Ordernoneinsertion (or access, with a flag)sorted by key
Null keysoneonenone
Needsequals/hashCode on keyssameComparable keys or a Comparator
Extra—removeEldestEntry → LRU cachefirstKey, floorKey, headMap, subMap, descendingMap

LinkedHashMap threads a doubly linked list through the entries; with accessOrder = true every get moves the entry to the end, and overriding removeEldestEntry gives a bounded LRU cache in five lines (Module 13's exercise). TreeMap is a red-black tree keyed by compareTo — the navigation methods are its reason to exist.

The modern API

Map<String, Integer> counts = new HashMap<>();

// counting — the old way, with a null check
Integer c = counts.get(word);
counts.put(word, c == null ? 1 : c + 1);

// counting — the modern way
counts.merge(word, 1, Integer::sum);                     // put 1 if absent, else combine old and 1

// reading with a default
int n = counts.getOrDefault(word, 0);                    // no null, no unboxing NPE

// multimap: a list per key, created on first use
Map<String, List<String>> byInitial = new HashMap<>();
byInitial.computeIfAbsent(word.substring(0, 1), k -> new ArrayList<>()).add(word);

// insert only if absent
counts.putIfAbsent("x", 0);                              // returns the existing value or null

// recompute in place
counts.compute("x", (k, v) -> v == null ? 1 : v * 2);
counts.computeIfPresent("x", (k, v) -> v > 100 ? null : v);   // returning null REMOVES the entry

// bulk
counts.forEach((k, v) -> System.out.println(k + "=" + v));
counts.replaceAll((k, v) -> v * 10);
counts.entrySet().removeIf(e -> e.getValue() == 0);

merge(key, value, fn): if absent, store value; else store fn(old, value); a null result removes the key. computeIfAbsent(key, fn): if absent (or mapped to null), store and return fn(key); else return the existing value — and the mapping function must not modify the map. These two replace the majority of if (map.containsKey(...)) code.

Iterating

for (Map.Entry<String, Integer> e : counts.entrySet()) {     // the efficient way: one lookup per entry
    e.getKey(); e.getValue(); e.setValue(e.getValue() + 1);    // setValue writes through
}
for (String k : counts.keySet()) counts.get(k);              // two lookups per entry — avoid
for (int v : counts.values()) …
counts.forEach((k, v) -> …);

keySet(), values() and entrySet() are views: removing from them removes from the map (counts.keySet().removeIf(...), counts.values().remove(0) removes one entry with value 0), and adding to them is unsupported. Modifying the map while iterating a view throws ConcurrentModificationException — except through the iterator's own remove or entry.setValue.

TreeMap navigation

TreeMap<Integer, String> t = new TreeMap<>();
t.firstKey(); t.lastKey(); t.firstEntry();
t.floorKey(25); t.ceilingKey(25); t.lowerKey(20); t.higherKey(20);
t.headMap(30); t.tailMap(30, true); t.subMap(10, 30);       // views
t.pollFirstEntry(); t.descendingMap();

Same navigation as TreeSet, with entries. Interval lookups, "the price valid at date d" (floorEntry(d)), leaderboards and ordered reports are TreeMap jobs.

Choosing a key type

Keys must be immutable with correct equals/hashCode (hash maps) or a consistent compareTo (tree maps): String, Integer, Long, enums, records, UUID, LocalDate. Composite keys are records (record Cell(int r, int c)). Never arrays (identity equality), never mutable objects.

Map.of, Map.entry, copies

Map.of("a", 1, "b", 2) (up to 10 pairs) and Map.ofEntries(Map.entry("a", 1), …) are immutable, null-hostile, order-randomised. Map.copyOf copies; Collections.unmodifiableMap wraps. new HashMap<>(other) copies into a mutable map.

Interview angle

  • "How does HashMap work?" Hash → bucket index; chain (then tree) of nodes; equals to confirm; resize at 0.75 load; O(1) average.
  • "What happens with a bad hashCode?" Everything lands in one bucket: O(n) lookups, or O(log n) once treeified.
  • "Why is 0.75 the load factor?" A time/space compromise: fewer collisions than 1.0, less waste than 0.5.
  • "HashMap vs Hashtable vs ConcurrentHashMap?" Unsynchronised / legacy fully synchronised / modern concurrent with striped locking (Module 17).
  • "How do you count words?" merge(word, 1, Integer::sum).
  • "Can a HashMap have a null key?" One. TreeMap and ConcurrentHashMap cannot.

Key takeaways

  • HashMap: buckets by spread hashCode, chains that treeify, resize at 0.75 — O(1) with good hashes; immutable keys with consistent equals/hashCode.
  • LinkedHashMap for order and LRU; TreeMap for sorted keys and floorKey-style navigation; none of the three is thread-safe.
  • getOrDefault, merge, computeIfAbsent, putIfAbsent, compute replace null-check boilerplate; a null result from compute*/merge removes.
  • Iterate entrySet(); the views write through and are fail-fast.

Common questions

What happens when two keys have the same hash in a HashMap?

They collide in one bucket, and the map keeps both nodes in that bucket's chain, telling them apart with equals. Lookups walk the chain, so many collisions degrade towards O(n); since Java 8 a chain longer than eight is converted to a red-black tree, bounding it at O(log n) for Comparable keys.

What is the difference between HashMap, LinkedHashMap and TreeMap?

HashMap is O(1) on average with no iteration order. LinkedHashMap adds insertion order, or access order with a flag, which together with removeEldestEntry makes an LRU cache. TreeMap keeps keys sorted in a red-black tree at O(log n), with navigation such as floorKey and headMap.

Can a HashMap have a null key?

Yes, one. HashMap allows a single null key and any number of null values, which makes get returning null ambiguous — use containsKey to tell an absent key from a null value. TreeMap and ConcurrentHashMap do not allow null keys.

What does computeIfAbsent do in Java?

computeIfAbsent(key, fn) returns the value for key, first storing fn(key) if the key is absent or mapped to null. It is the multimap idiom: map.computeIfAbsent(k, x -> new ArrayList<>()).add(v) creates each list on first use. For counting, merge(word, 1, Integer::sum) does the same job.

Why is the HashMap load factor 0.75?

It is a compromise between time and space: a load factor of 1.0 would mean more collisions, and 0.5 would waste more of the table. When the number of entries exceeds capacity times 0.75, the table doubles and every entry is redistributed.

Exercises

Grouping and counting with the modern API

Read an integer n and n lines <city> <name>. Build (1) a Map<String, Integer> of how many people per city using merge, (2) a Map<String, List<String>> of names per city using computeIfAbsent, and (3) a TreeMap copy of the counts. Print, for every city in sorted order, <city> <count>: <names in input order joined by ,>, then largest=<the city with the most people; ties by name via TreeMap iteration>, then floor=<the TreeMap's floorKey for the string "p", or none>.

Example: 3 then paris ada, oslo bob, paris cy →

oslo 1: bob
paris 2: ada,cy
largest=paris
floor=oslo

An inverted index

Read lines of text until end of input (line numbers start at 1). Build an inverted index: TreeMap<String, TreeSet<Integer>> from each lower-cased word to the set of lines it appears on, using computeIfAbsent. Print <word>: <lines> for every word, then vocabulary=<size> and mostSpread=<the word on the most lines; ties by natural order>.

Example input

the cat
The dog
a cat

→

a: [3]
cat: [1, 3]
dog: [2]
the: [1, 2]
vocabulary=4
mostSpread=cat

In this module: Collections framework

← Sets — HashSet, LinkedHashSet and TreeSet · Queues, deques and PriorityQueue →