🔑 Section 10 · Question #6

What Happens When 8 Elements Are Reached in One Bucket

When a single bucket accumulates 8 elements (TREEIFY_THRESHOLD = 8), HashMap initiates a search optimization procedure by invoking its internal treeifyBin() method. However, con...


🟢 Junior Level

Short Interview Answer (30 Seconds)

When a single bucket accumulates 8 elements (TREEIFY_THRESHOLD = 8), HashMap initiates a search optimization procedure by invoking its internal treeifyBin() method. However, converting the bucket into a Red-Black Tree occurs only conditionally:

  1. If total table capacity is less than 64 (table.length < 64): A tree is NOT created. Instead, HashMap calls resize(), doubling the capacity of the bucket array. JDK architects established that in small tables, collisions are caused by a lack of buckets; doubling the table redistributes colliding elements into separate buckets.
  2. If total table capacity is 64 or greater (table.length >= 64): The singly linked list in that bucket is converted into a balanced Red-Black Tree (TreeNode), reducing worst-case search complexity from linear $O(n)$ to logarithmic $O(\log n)$.

Comparison Table: Behavior Fork at 8 Elements

Evaluation Criterion Table Capacity $< 64$ (MIN_TREEIFY_CAPACITY) Table Capacity $\ge 64$
treeifyBin() Reaction Calls resize() (doubles array capacity) Converts bucket list into a Red-Black Tree
Bucket State Mutation Elements are redistributed across 2 separate buckets Elements remain in the same bucket as TreeNodes
Lookup Complexity Returns to $O(1)$ in the newly separated buckets Bounded to $O(\log k)$ within the current bucket
Memory Allocation Allocates a new array of references table[] Replaces Node with heavier TreeNode instances (~2x RAM)

Java 21 Demonstration

import java.lang.reflect.Field;
import java.util.HashMap;

public class TreeifyThresholdDemo {

    // Key with a constant hashCode to force deterministic collisions
    record BadKey(int id) {
        @Override
        public int hashCode() {
            return 42; // All instances hash to the exact same bucket!
        }
    }

    public static void main(String[] args) throws Exception {
        HashMap<BadKey, String> map = new HashMap<>();

        // Insert 8 colliding keys into a map with initial capacity 16
        for (int i = 1; i <= 8; i++) {
            map.put(new BadKey(i), "Val-" + i);
        }

        // When the 8th element was inserted, table capacity was 16 (< 64).
        // Instead of treeifying, resize() occurred to expand the table!
        printTableCapacity(map); // Prints capacity = 64 (after sequential resizes: 16 -> 32 -> 64)
    }

    private static void printTableCapacity(HashMap<?, ?> map) throws Exception {
        Field tableField = HashMap.class.getDeclaredField("table");
        tableField.setAccessible(true);
        Object[] table = (Object[]) tableField.get(map);
        System.out.println("Current table capacity (table.length): " + (table != null ? table.length : 0));
    }
}

🟡 Middle Level

Why resize() is Preferred Over Treeification

When designing HashMap, engineers prioritized array expansion over tree construction for two fundamental reasons:

  1. True Collision Resolution: Doubling table capacity ($oldCap \to 2 \times oldCap$) gives every element a 50% probability of moving to a new bucket (oldIndex + oldCap) based on the single bit test (hash & oldCap). This resolves the collision at its root, returning lookups to immediate $O(1)$ constant time.
  2. Memory Preservation: A TreeNode is roughly double the memory size of a standard Node (56–64 bytes vs. 32 bytes). Treeifying a cramped table treats the symptom (slow lookup) while exacerbating memory bloat and slowing down subsequent insertions due to rebalancing rotations.

Thresholds 8 and 6: Probability Theory and Hysteresis

The OpenJDK source defines three tightly coupled constants:

  • TREEIFY_THRESHOLD = 8 — bucket depth triggering treeifyBin();
  • UNTREEIFY_THRESHOLD = 6 — bucket depth triggering reversion to a list;
  • MIN_TREEIFY_CAPACITY = 64 — minimum table capacity required before treeifying.

1. Statistical Derivation of Threshold 8 (Poisson Distribution):

Under standard load factor $0.75$, key placement across buckets follows a Poisson distribution with $\lambda = 0.5$. The probability of 8 entries hashing into the same bucket purely by chance is: \(P(8) = \frac{e^{-0.5} \times 0.5^8}{8!} \approx 0.00000006 \quad (6 \times 10^{-8})\) This is an exceedingly rare statistical anomaly. If it occurs, the table is either overcrowded or under an adversarial Hash Flooding DoS attack.

2. The 2-Unit Hysteresis Gap (8 vs. 6):

If the conversion thresholds were adjacent (e.g., 8 to treeify and 7 to untreeify), an application regularly inserting and deleting an 8th entry would cause structure thrashing (flapping): continuous, costly conversions back and forth between lists and trees. The 2-element gap stabilizes performance.


🔴 Senior Level

Under the Hood: treeifyBin() Execution Mechanics

Here is the exact transformation sequence inside HotSpot:

[ Head Node 1 ] ──next──► [ Node 2 ] ──next──► [ Node 3 ] ... (8 elements)
                               │
                       treeifyBin() (table.length >= 64)
                               │
                               ▼
        1. Replace Node with TreeNode and link doubly linked list:
[ TreeNode 1 ] ◄──prev/next──► [ TreeNode 2 ] ◄──prev/next──► [ TreeNode 3 ] ...
                               │
                               ▼
        2. Invoke TreeNode.treeify() — construct balanced Red-Black Tree:
                          [ Root: TreeNode 4 ] (Black)
                                 ╱            ╲
                      [ TreeNode 2 ]        [ TreeNode 6 ] (Red)
                       ╱          ╲          ╱          ╲
  1. Pass 1 (Type Replacement): Traverses the singly linked list, replacing each Node with a TreeNode, preserving existing next pointers while initializing prev pointers to form a doubly linked list.
  2. Pass 2 (TreeNode.treeify()): The first node is established as the tree root. Remaining nodes are inserted sequentially into the binary tree, calling balanceInsertion() to execute left/right rotations and recoloring.
  3. The moveRootToFront() Operation: Rotations may change the root node. The moveRootToFront() helper relocates the new tree root to the head of the doubly linked list and updates table[index] = root. This ensures that subsequent searches begin at the tree root in $O(1)$ without traversing prev links.

Key Ordering Architecture: Comparable and tieBreakOrder

Binary search trees require total ordering ($<$ or $>$) among elements.

// HotSpot reflection check for Comparable implementation:
static Class<?> comparableClassFor(Object x) {
    if (x instanceof Comparable) {
        Class<?> c = x.getClass();
        if (c == String.class) // Fast-path shortcut for String
            return c;
        Type[] ts = c.getGenericInterfaces();
        // Deep reflection: verifies that Comparable is parameterized with the exact class!
    }
    return null;
}

What Happens When Keys Do NOT Implement Comparable:

  1. If keys belong to different classes, they are ordered by class name: a.getClass().getName().compareTo(b.getClass().getName()).
  2. If class names match or compareTo() returns 0 (while equals() is false), HotSpot invokes tieBreakOrder():
    static int tieBreakOrder(Object a, Object b) {
        int d = (System.identityHashCode(a) <= System.identityHashCode(b) ? -1 : 1);
        return d;
    }
    

    The final tie-breaker is System.identityHashCode(), comparing the raw memory address hash from the object header’s Mark Word.


Treeification in Production as an Architectural Anomaly Alert

In enterprise APM monitoring, the emergence of TreeNode instances should trigger alerts:

  • Non-Selective Keys: An engineer overrode hashCode() based on a low-cardinality field (e.g., an OrderStatus enum with only 5 values).
  • GC Pressure in Young Generation: Repeated tree insertions generate multiple node wrapper allocations and tree balance operations, contributing to Stop-The-World (GC STW) pauses.

🎯 Interview Cheat Sheet

1. 30-Second Elevator Pitch

When a bucket reaches 8 elements, treeifyBin() is called. If total capacity is less than 64 (MIN_TREEIFY_CAPACITY), a tree is not created—instead, resize() is invoked, doubling table capacity to redistribute entries across new buckets. If capacity is $\ge 64$, the bucket transforms into a balanced Red-Black Tree (TreeNode), reducing lookup complexity from $O(n)$ to $O(\log n)$. When occupancy drops to $\le 6$, the tree reverts back to a linked list (hysteresis).


2. 4 Tricky Questions with Deep Answers

Question 1: Under what scenario will a bucket with 8 elements NOT be converted into a Red-Black Tree, and what happens instead?

Answer: A tree is not created if the current array capacity table.length < 64. Instead, HashMap invokes resize(), doubling the capacity (e.g., from 16 to 32, or 32 to 64). During doubling, elements in the overflowing bucket are split into two separate buckets using the bit test (hash & oldCap) == 0. In almost all natural cases, this eliminates the local collision without incurring the memory and CPU overhead of building a TreeNode tree.

Question 2: Why is key instanceof Comparable insufficient for HashMap, and why does comparableClassFor() inspect generic interfaces via reflection?

Answer: Because Comparable<T> is generic. A class could implement Comparable<OtherClass> rather than Comparable<Self>. For a binary search tree to order nodes safely, the key must be comparable against instances of its own exact class. comparableClassFor() analyzes c.getGenericInterfaces() to confirm that the generic type parameter matches Class<Self>, falling back to tieBreakOrder() if it does not.

Question 3: Why does HashMap invoke moveRootToFront() after tree balancing rotations?

Answer: The slot table[index] holds a direct reference to the entry point of the bucket. Balancing rotations during tree insertions frequently change which node serves as the tree root. moveRootToFront() detaches the new root, places it at the head of the node list, and writes it directly into table[index]. This guarantees that future get() or put() operations access the root node in $O(1)$ without traversing prev pointers.

Question 4: Why does the appearance of TreeNode objects in production heap dumps indicate a design flaw?

Answer: Under a uniform hash code, the probability of 8 entries colliding in a single bucket is $6 \times 10^{-8}$ (Poisson distribution). The presence of TreeNode instances in production indicates either a broken, low-entropy hashCode() (e.g., hashing a non-selective field or returning a constant) or an external Algorithmic Complexity / Hash Flooding DoS attack. While treeification prevents complete server failure, it signals a defect in key distribution.


3. Common Pitfalls and Red Flags

❌ “A bucket always becomes a Red-Black Tree as soon as it reaches 8 elements.”
Correction: Only if total table capacity table.length >= 64. If capacity is below 64, resize() is invoked instead.

❌ “A tree reverts back to a list as soon as it drops below 8 elements.”
Correction: Untreeification occurs at 6 (UNTREEIFY_THRESHOLD). The 2-element gap provides hysteresis to prevent structural thrashing.

❌ “Treeification eliminates collisions.”
Correction: Treeification only optimizes search within an existing collision chain from $O(n)$ to $O(\log n)$. Resizing is the only mechanism that resolves collisions.