Back to HashMap Internals
Q10Question 10 of 10

How does HashMap resize internally?

Key Concepts
  • Resize triggers when size exceeds capacity times load factor (default: 12 entries for capacity 16 with load factor 0.75)
  • Capacity always doubles to the next power of 2 (16 to 32 to 64 to 128), enabling fast bitwise bucket indexing
  • Bucket index is hash & (capacity - 1), equivalent to hash % capacity but using bitwise AND which is ~10x faster than modulo
  • Java 8 optimizes redistribution: each entry either stays at its current index or moves to oldIndex + oldCapacity, decided by checking one bit
  • The one-bit check: hash & oldCapacity; if 0, entry stays; if 1, entry moves to oldIndex + oldCapacity
  • Resize is O(n) because every entry is visited; pre-sizing avoids this cost for known data volumes
  • Load factor 0.75 balances space efficiency (higher wastes less) and lookup speed (lower has fewer collisions)

Definition

HashMap resizes by doubling capacity (always power-of-2) and redistributing entries using a one-bit check: hash & oldCapacity determines if an entry stays at its index or moves to index + oldCapacity.
  • Trigger: resize when size > capacity * loadFactor (default threshold = 16 * 0.75 = 12)
  • New capacity: doubles the old capacity (16 → 32 → 64); always a power of 2
  • Bucket index: hash & (capacity - 1) – bitwise AND is ~10x faster than modulo; power-of-2 capacity makes this work
  • Java 8 redistribution: for each entry, check hash & oldCapacity; if 0, stay at current index; if 1, move to oldIndex + oldCapacity
  • Cost: O(n) – every entry is visited and potentially moved
  • Pre-sizing: new HashMap<>(expectedSize * 4/3 + 1) avoids resize entirely

A filing cabinet has 16 drawers. When 12 drawers are used (75%), buy a cabinet with 32 drawers (double). Moving files: each file either stays in its current drawer or moves to drawer currentDrawer + 16. This is determined by looking at one extra bit in the file's code – no need to recalculate the full drawer assignment from scratch. Why powers of 2? Because hash & 15 (for 16 drawers) is one CPU instruction, much faster than hash % 16 which requires division.

Understanding the Concepts

When does HashMap resize?

HashMap maintains a threshold field equal to capacity * loadFactor. When size exceeds this threshold after a put operation, resize is triggered.

With default settings (capacity 16, load factor 0.75), the first resize happens when the 13th entry is added (threshold = 12). After resize, capacity becomes 32 and threshold becomes 24.

JAVA
package com.bank.demo;

import java.util.*;

public class ResizeTriggerDemo {
    public static void main(String[] args) {
        // Capacity 16, load factor 0.75 → threshold 12
        Map<String, String> map = new HashMap<>(16, 0.75f);

        for (int i = 1; i <= 15; i++) {
            map.put("ACC" + i, "Holder" + i);
            // Resize happens when 13th entry is added
            // (size > 12 after 12th entry triggers resize on 13th put)
        }

        System.out.println("Size: " + map.size());
        // Capacity is now 32 (doubled from 16)
        // Threshold is now 24 (32 * 0.75)
    }
}
TEXT
Size: 15

The growth sequence for default settings:

| Capacity | Threshold | Resize at entry |
|---|---|---|
| 16 | 12 | 13th |
| 32 | 24 | 25th |
| 64 | 48 | 49th |
| 128 | 96 | 97th |
| 256 | 192 | 193rd |
| 1024 | 768 | 769th |

Why is capacity always a power of 2?

The bucket index is computed as hash & (capacity - 1). When capacity is a power of 2, capacity - 1 is a bitmask with all lower bits set:

DIAGRAM
- capacity 16 → mask 15 = `0b00001111`
- capacity 32 → mask 31 = `0b00011111`
- capacity 64 → mask 63 = `0b00111111`

Bitwise AND with this mask extracts the lower bits of the hash, which is equivalent to hash % capacity. But bitwise AND is a single CPU instruction, while modulo requires integer division – roughly 10x slower.

JAVA
package com.bank.demo;

public class BitwiseVsModuloDemo {
    public static void main(String[] args) {
        int hash = "SAV2001".hashCode();
        int capacity = 16;

        // These produce the same result:
        int bitwiseIndex = hash & (capacity - 1); // fast: one instruction
        int moduloIndex = Math.floorMod(hash, capacity); // slow: division

        System.out.println("Bitwise: " + bitwiseIndex);
        System.out.println("Modulo:  " + moduloIndex);
        System.out.println("Same: " + (bitwiseIndex == moduloIndex));

        // Performance comparison
        int iterations = 100_000_000;
        long start = System.nanoTime();
        int sum = 0;
        for (int i = 0; i < iterations; i++) {
            sum += i & 15; // bitwise
        }
        long bitwiseTime = System.nanoTime() - start;

        start = System.nanoTime();
        sum = 0;
        for (int i = 0; i < iterations; i++) {
            sum += i % 16; // modulo
        }
        long moduloTime = System.nanoTime() - start;

        System.out.printf("Bitwise: %d ms%n", bitwiseTime / 1_000_000);
        System.out.printf("Modulo:  %d ms%n", moduloTime / 1_000_000);
    }
}
TEXT
Bitwise: 3
Modulo:  3
Same: true
Bitwise: 45 ms
Modulo:  128 ms

How does Java 8 redistribute entries efficiently?

Before Java 8, every entry was rehashed with the new mask: hash & (newCapacity - 1). This required recomputing the index for every entry.

Java 8 introduced an optimization. When capacity doubles from n to 2n, the new mask has one more bit than the old mask. For each entry, only that one extra bit matters:

  • If the extra bit in the hash is 0, the entry stays at its current index.
  • If the extra bit is 1, the entry moves to oldIndex + oldCapacity.

The check is simply hash & oldCapacity. This splits each bucket into exactly two groups with one bit check per entry – no full rehash needed.

JAVA
// Capacity 16 → 32
// Old mask: 0b01111 (15)
// New mask: 0b11111 (31)
// Extra bit: bit 4 (value 16 = oldCapacity)

// Entry with hash 0b...01011:
//   Old index: hash & 15 = 01011 & 01111 = 01011 = 11
//   Check:     hash & 16 = 01011 & 10000 = 00000 = 0 → STAYS at index 11

// Entry with hash 0b...11011:
//   Old index: hash & 15 = 11011 & 01111 = 01011 = 11
//   Check:     hash & 16 = 11011 & 10000 = 10000 ≠ 0 → MOVES to index 11 + 16 = 27

What is the impact of load factor?

The load factor controls the trade-off between space and time:

| Load factor | Space | Collisions | Resize frequency |
|---|---|---|---|
| 0.50 | 50% wasted | Very few | Frequent (more resizes) |
| 0.75 (default) | 25% wasted | Few | Balanced |
| 1.00 | 0% wasted | Many | Rare (more collisions) |
| 1.50 | Overpacked | Very many | Very rare |

The default 0.75 is well-studied and works for most applications. Lower values waste memory but give faster lookups. Higher values save memory but increase collision chains.

How do you pre-size HashMap correctly?

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.*;

public class PreSizingDemo {
    public static void main(String[] args) {
        int expectedAccounts = 100_000;

        // Formula: expectedSize / loadFactor + 1
        int correctCapacity = (int) (expectedAccounts / 0.75) + 1; // 133334
        Map<String, BigDecimal> balances = new HashMap<>(correctCapacity);
        // HashMap rounds up to next power of 2: 262144
        // Threshold = 262144 * 0.75 = 196608 > 100000 → no resize!

        // Simpler: expectedSize * 4/3 + 1 (integer math for 0.75)
        Map<String, BigDecimal> alt = new HashMap<>(expectedAccounts * 4 / 3 + 1);

        // Java 19+: factory method
        // Map<String, BigDecimal> modern = HashMap.newHashMap(expectedAccounts);

        // Load data without resizing
        for (int i = 0; i < expectedAccounts; i++) {
            balances.put("ACC" + String.format("%06d", i), BigDecimal.valueOf(i));
        }
        System.out.println("Loaded " + balances.size() + " accounts without resizing");
    }
}
TEXT
Loaded 100000 accounts without resizing

What are common mistakes?

Mistake 1: Passing the expected size directly as capacity

JAVA
// WRONG: new HashMap<>(10000) with 10K entries
// Capacity 16384 (next power of 2), threshold 12288
// 10000 < 12288 → no resize. Happens to work, but wastes capacity.

// WRONG: new HashMap<>(1000) with 1000 entries
// Capacity 1024, threshold 768
// 1000 > 768 → resize at 769th entry!

// CORRECT: account for load factor
// new HashMap<>(1000 * 4 / 3 + 1) = new HashMap<>(1334)
// Capacity 2048, threshold 1536 → no resize for 1000 entries

What should a senior engineer know?

- Resize cost. For a HashMap with 10 million entries, resize copies 10 million entries to a new table. Each entry's bucket position may change. This is O(n) and can cause a noticeable latency spike.

- Non-power-of-2 initial capacity. If you pass a non-power-of-2 initial capacity (e.g., 1000), HashMap rounds up to the next power of 2 (1024). This is handled by tableSizeFor() which uses bit manipulation to find the next power of 2.

- Maximum capacity. HashMap's maximum capacity is 1 << 30 (1,073,741,824). Beyond this, the threshold is set to Integer.MAX_VALUE to prevent further resizing.

- The hi/lo chain split. During Java 8 resize, each bucket is split into two chains: lo (entries that stay) and hi (entries that move). These chains are built by iterating the original chain once, then the lo chain is placed at the old index and the hi chain at oldIndex + oldCapacity.

What are common interview follow-up questions?

  • "Why is capacity a power of 2?" – Enables fast bitwise AND instead of modulo for bucket indexing.
  • "What triggers a resize?" – size exceeding capacity * loadFactor.
  • "How does Java 8 optimize resize?" – One-bit check (hash & oldCapacity) instead of full rehash.
  • "How do you avoid resize?" – Pre-size with new HashMap<>(expectedSize / 0.75 + 1).
  • "What is the default load factor?" – 0.75.

Visual Overview

DIAGRAM
  Resize: capacity 16 → 32

  Old mask: 0b01111 (15)      New mask: 0b11111 (31)
  Extra bit:         ^ (bit 4 = 16 = oldCapacity)

  For each entry in bucket 3:
    hash & 16 == 0?  → stays at index 3
    hash & 16 != 0?  → moves to index 3 + 16 = 19

  Before resize (16 buckets):
  [0] [1] [2] [3:A,B,C] [4] ... [15]

  After resize (32 buckets):
  [0] [1] [2] [3:A,C] [4] ... [15] [16] [17] [18] [19:B] [20] ... [31]
              lo chain                              hi chain

  Pre-sizing formula:
  capacity = expectedSize / 0.75 + 1
  HashMap rounds up to next power of 2

Key Takeaway

HashMap resize is triggered when the number of entries exceeds capacity times load factor. Capacity always doubles to the next power of 2, enabling the fast bitwise AND trick for bucket indexing: hash & (capacity - 1) instead of the slower hash % capacity.

Java 8 made resize smarter. Instead of rehashing every entry, it checks one bit: hash & oldCapacity. If that bit is 0, the entry stays at its current index. If 1, it moves to oldIndex + oldCapacity. This splits each bucket into exactly two groups with a single bit operation per entry.

In the National Bank of India's account cache, Rajesh pre-sizes the HashMap to avoid resize during the EOD batch load of 2 million records. Without pre-sizing, the map resizes about 20 times, each time copying millions of entries to a new table. With new HashMap<>(2_700_000), the map allocates enough capacity upfront and never resizes.

Remember one sentence: HashMap doubles its power-of-2 capacity at 75% load, and Java 8 redistributes entries with a single-bit check instead of full rehashing.

---

Share
Use ← → arrow keys to navigate