skip to content
everything.deepDives.hashmapInternals

HashMap, line by line

hash spreading, (n-1) & hash, the resize split, and treeification.

chapter
chapter 17.01
reading time
14 min read

a HashMap is a Node[] whose length is always a power of two. a key's bin is (n - 1) & hash, where hash = h ^ (h >>> 16) folds the high bits of hashCode() into the low ones. when it gets 75% full the array doubles, and every chain splits by one bit. a bin that collects 9 nodes becomes a red-black tree, but only once the table has at least 64 bins.

every number in this chapter is a constant in java/util/HashMap.java (jdk 25), or something we ran.

the picture

the HashMap chapter has the friendly version: buckets, collisions, a load factor. this one reads the source. if you've ever been asked "why is the capacity a power of two?" in an interview, everything you need is in about 40 lines of it.

think of a coat check with numbered hooks (the table). your ticket number decides the hook (the hash picks the bin). two coats on one hook get stacked (a chain). when the room gets busy, the staff build a second row of hooks and move each coat at most once, to a hook they can work out from the ticket alone (resize).

stack
main
map
heap
HashMap
table
size3
threshold12
loadFactor0.75f
modCount3
Node[16]
null0
1
2
null3
null4
null5
null6
null7
null8
null9
null10
null11
null12
null13
null14
null15
HashMap$Node
hash97
key"a"
value1
next
HashMap$Node
hash113
key"q"
value3
nextnull
HashMap$Node
hash98
key"b"
value2
nextnull
three entries, two of them colliding in bin 1. keys and values are references too; they're drawn inline to keep the picture small. the blue next arrow is the whole collision strategy.
table
Node<K,V>[]. null until the first put (it's allocated lazily, inside resize()). length is always a power of two, default 16.
Node
four fields: final int hash, final K key, V value, Node next. 32 bytes each on jdk 25 with default flags (we measured it with a class histogram).
threshold
capacity × loadFactor. default 16 × 0.75 = 12. the put that makes size exceed it triggers a resize.
modCount
bumped on every structural change. iterators compare it to fail fast (see fail-fast).

notice the node keeps its hash. it's computed once, on the way in, and never again. that detail pays off twice later: lookups compare the cached int before ever calling equals, and resizing never calls hashCode().

hash(): folding the top half down

the whole method is one line:

HashMap.java (jdk 25)
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

why bother? because of how the bin is picked. a 16-bin table looks at the low 4 bits of the hash and nothing else. if your keys only differ above bit 4 (and plenty do: whole-number Float keys, ids that are multiples of 65,536, a lazy hashCode()), they all land in the same bin. h >>> 16 slides the top half down, and XOR mixes it into the bits the mask will actually read.

key =
n =
spread =
h = key.hashCode()
h >>> 16
hash = h ^ (h >>> 16)
n - 1 = 15
index = hash & (n - 1)
lands inbin 11the low 4 bits of hash: h's bits 0 to 3 XOR its bits 16 to 19.
16 Integer keys: 0, 1 << 16, 2 << 16 … 15 << 1616 bins used
pick a key and a table size, then flip spread off. the amber bits are the ones that decide the bin. the strip at the bottom is sixteen Integer keys that only differ above bit 16: without spreading they all pile into bin 0.

same thing, in code you can run:

java
void main() {
    for (int i = 0; i < 4; i++) {
        int h = Integer.valueOf(i << 16).hashCode();
        int spread = h ^ (h >>> 16);
        IO.println(h + ": bin " + (h & 15) + " without spreading, bin " + (spread & 15) + " with");
    }
}
output
0: bin 0 without spreading, bin 0 with
65536: bin 0 without spreading, bin 1 with
131072: bin 0 without spreading, bin 2 with
196608: bin 0 without spreading, bin 3 with

the comment above hash() is honest about the tradeoff: it's the cheapest mix that fixes the common systematic cases, and anything nastier is left for the trees (below) to absorb. one shift, one XOR. it runs on every get and put, so cheap wins.

(n - 1) & hash, and why the length is a power of two

when n is a power of two, n - 1 is a run of ones: 16 → 1111, 64 → 111111. so hash & (n - 1) keeps exactly the low bits, which is the same as hash % n for a non-negative hash, without a division. and unlike %, it never goes negative (-7 % 16 is -7; -7 & 15 is 9), so there's no Math.abs bug waiting to happen.

that's the deal: a power-of-two length makes indexing a single AND, and (we'll see in a second) it makes resizing almost free. the price is that only the low bits matter, which is why hash() exists.

so what if you ask for a capacity that isn't a power of two? tableSizeFor rounds up, with a bit trick instead of a loop:

n =
100
cap - 1 = 99
-1 >>> 25
+ 1 = 128

every bit below the highest set bit of cap - 1 becomes a 1, then + 1 carries it up to the next power of two.

new HashMap<>(100)
tableNode[128]fits96 before resizing100 puts1 resize, ends at Node[256]
HashMap.newHashMap(100)
tableNode[256]fits192 before resizing100 putsno resize
drag n. -1 >>> numberOfLeadingZeros(cap - 1) is all ones up to the highest bit of cap - 1; adding one carries into the next power of two. the cards below show what that means for 100 entries.
predict the output
java
var map = new HashMap<Integer, Integer>(100);
for (int i = 0; i < 100; i++) map.put(i, i);
// how long is map's table now?

put, one branch at a time

putVal is about 40 lines and every one of them shows up in an interview somewhere. here's the path a key takes:

hover a stage.

hover a stage. the order of the checks in walk bin matters: comparing two ints is nearly free, equals() might not be.

null keys are allowed, and they really do go to bin 0 (unlike ConcurrentHashMap, which bans them):

java
void main() {
    var m = new HashMap<String, Integer>();
    m.put(null, 1);
    m.put(null, 2);
    IO.println(m + " " + m.get(null) + " " + m.size());
    IO.println("Aa".hashCode() + " " + "BB".hashCode());
}
output
{null=2} 2 1
2112 2112

resize: every chain splits by one bit

here's the payoff of power-of-two lengths. going from 16 to 32 bins, the mask grows by exactly one bit, bit 4 (value 16, the old capacity). a node's new index is its old index plus either 0 or 16, depending on that one bit. so resize() doesn't rehash anything. it walks each old chain once and deals the nodes into two lists: (e.hash & oldCap) == 0 stays at j (lo), the rest go to j + oldCap (hi).

old table · Node[16]
0
016
1
2
2
3
4
5
5213753
6
7
7
8
9
25
10
11
12
1244
13
14
3062
15
↓ resize(): newCap = 32, threshold = 24
new table · Node[32], bins 0 to 15 (lo)
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 bins 16 to 31 (hi = j + 16)
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
the bit that will decide is bit 4 (value 16, the old capacity). nothing has moved yet.

13 entries in 16 bins. the threshold is 12 (16 × 0.75), so the 13th put (key 16, into bin 0) pushed size past it and called resize().

step 1/9
step through a real resize: these 13 keys, in this order, in a default HashMap. nodes in the bin being split are amber. watch the columns: a node either stays in its column on the top row or drops straight down to the bottom one.
quick check
a node sits in bin 5 of a 32-bin table. the table doubles to 64. where can the node end up?

this is also why HashMap iteration order is "whatever the bins say", and why it changes when the map grows:

predict the output
java
var m = new HashMap<Integer, String>();
m.put(17, "a");
m.put(1, "b");
IO.println(m.keySet());
for (int i = 100; i < 111; i++) m.put(i, "x"); // 13 entries: resize to 32
m.keySet().removeIf(k -> k >= 100);
IO.println(m.keySet());
one level deeper:the java 7 resize loop, and why it's gone+

java 7's resize moved each node to the head of its new chain, which reversed the chain. if two threads resized the same map at once, one could see the other's half-reversed links and stitch a.next = b, b.next = a. the next get on that bin spun forever at 100% CPU. java 8's lo/hi lists keep the original order (the source literally says "preserve order"), so that particular cycle can't form. a HashMap shared between threads is still broken: lost puts, a size that lies, entries that vanish. the fix isn't a better HashMap, it's ConcurrentHashMap.

treeification: when a chain turns into a tree

three constants control it, all in the source:

constantvaluewhat it does
TREEIFY_THRESHOLD8appending to a bin that already has 8 nodes calls treeifyBin
MIN_TREEIFY_CAPACITY64treeifyBin on a table smaller than this resizes instead
UNTREEIFY_THRESHOLD6when a resize splits a tree bin, a half with 6 or fewer nodes goes back to a list

the middle one is the bit people miss. a crowded bin in a small table is more likely a "the table is too small" problem than a "these keys really collide" problem, so HashMap tries growing first. drag through it with keys that all return the same hashCode():

keys =
8
table =
Node[16] · bin 10
bin =
Node, linked list · 32 bytes per node

8 nodes, which is TREEIFY_THRESHOLD. the next append is the one that checks it.

drag the slider. at 9 and 10 the bin doesn't treeify, the table grows instead. at 11 the table is finally 64 wide and the list becomes a real red-black tree (these are the actual shapes jdk 25 built). the blue links are next, then left/right pointers.

we didn't make those numbers up. reflection into a live map (this needs --add-opens, so it's a terminal run rather than a run button):

Treeify.java
import java.lang.reflect.Field;
import java.util.HashMap;
 
public class Treeify {
    record Key(int id) implements Comparable<Key> {
        public int hashCode() { return 42; } // every key collides
        public int compareTo(Key o) { return Integer.compare(id, o.id); }
    }
 
    public static void main(String[] args) throws Exception {
        Field table = HashMap.class.getDeclaredField("table");
        table.setAccessible(true);
        var map = new HashMap<Key, Integer>();
        for (int i = 1; i <= 11; i++) {
            map.put(new Key(i), i);
            Object[] t = (Object[]) table.get(map);
            Object bin = t[42 & (t.length - 1)];
            System.out.println(i + " keys: Node[" + t.length + "], bin is a " + bin.getClass().getSimpleName());
        }
    }
}
terminal
$ java --add-opens java.base/java.util=ALL-UNNAMED Treeify.java
1 keys: Node[16], bin is a Node
...
8 keys: Node[16], bin is a Node
9 keys: Node[32], bin is a Node
10 keys: Node[64], bin is a Node
11 keys: Node[64], bin is a TreeNode

a TreeNode is 56 bytes against a Node's 32 (it extends LinkedHashMap.Entry and adds parent, left, right, prev and a red flag). that's why trees are the fallback, not the default. the comment in HashMap.java works out the odds: with random hashes and the default load factor, bin sizes follow a Poisson distribution with a mean of about 0.5, and the chance of a bin reaching 8 is about 6 in 100 million. if you see tree bins in a heap dump, some hashCode() is broken or someone is attacking you on purpose (hash flooding, where an attacker sends keys chosen to collide). the trees arrived in java 8 (JEP 180) so that a pile of colliding keys degrades to O(log n) instead of a list walk.

why 8 to treeify but 6 to untreeify? hysteresis. if both were 8, a bin hovering around 8 entries would flip between list and tree on every other operation. the gap of 2 means it has to shrink for real before converting back. (removing from a tree bin uses a shape check instead of a count: when the root has no left or right child, or the left child has no left child, the bin goes back to a list. that's somewhere between 2 and 6 nodes.)

keylines
  • hash = h ^ (h >>> 16). the mask only reads low bits, so the high bits get folded into them.
  • the bin is (n - 1) & hash. powers of two make that one AND, never negative.
  • new HashMap<>(n) is a capacity. for "n entries without resizing" use HashMap.newHashMap(n).
  • resize deals every chain into two: stay at j or move to j + oldCap. one bit decides, no rehash.
  • a 9th node in a bin treeifies only if the table has 64+ bins. otherwise the table doubles.
  • tree bins (56-byte nodes) are an emergency exit for bad hashes, not something healthy maps use.