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):
- Compute
h = key.hashCode(), spread its high bits into the low bits (h ^ (h >>> 16)), and take the bucket indexh & (table.length - 1). - If the bucket is empty, store a new node there.
- Otherwise walk the bucket's chain comparing
hashfirst and thenequals; replace the value if the key is found, else append a node. - If
sizeexceedscapacity × 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:
hashCodeandequalsmust agree (Module 8), orgetlooks 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
nullkey is allowed (bucket 0); null values are allowed — which is whygetreturning null is ambiguous (containsKeydisambiguates).
The three implementations
HashMap | LinkedHashMap | TreeMap | |
|---|---|---|---|
| get/put/remove | O(1) avg | O(1) avg | O(log n) |
| Order | none | insertion (or access, with a flag) | sorted by key |
| Null keys | one | one | none |
| Needs | equals/hashCode on keys | same | Comparable keys or a Comparator |
| Extra | — | removeEldestEntry → LRU cache | firstKey, 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
HashMapwork?" Hash → bucket index; chain (then tree) of nodes;equalsto 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.
- "
HashMapvsHashtablevsConcurrentHashMap?" Unsynchronised / legacy fully synchronised / modern concurrent with striped locking (Module 17). - "How do you count words?"
merge(word, 1, Integer::sum). - "Can a
HashMaphave a null key?" One.TreeMapandConcurrentHashMapcannot.
Key takeaways
HashMap: buckets by spreadhashCode, chains that treeify, resize at 0.75 — O(1) with good hashes; immutable keys with consistentequals/hashCode.LinkedHashMapfor order and LRU;TreeMapfor sorted keys andfloorKey-style navigation; none of the three is thread-safe.getOrDefault,merge,computeIfAbsent,putIfAbsent,computereplace null-check boilerplate; a null result fromcompute*/mergeremoves.- 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=osloAn 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=catIn this module: Collections framework
- The collections framework — the map and how to choose
- Lists — ArrayList, LinkedList and the List API
- Sets — HashSet, LinkedHashSet and TreeSet
- Maps — HashMap internals, TreeMap, LinkedHashMap and the modern API (this lesson)
- Queues, deques and PriorityQueue
- Sorting, Comparable and Comparator in depth
- Iterators, fail-fast behaviour and safe modification
- Immutable collections and the utility toolkit
- Checkpoint — Collections framework
← Sets — HashSet, LinkedHashSet and TreeSet · Queues, deques and PriorityQueue →