🔑 Section 10 · Question #16

What is Load Factor in HashMap

The Load Factor is a fractional metric (float) that defines how full the hash table is permitted to become before its bucket array is automatically doubled in size (a resize() o...


🟢 Junior Level

Short Interview Answer (30 seconds)

The Load Factor is a fractional metric (float) that defines how full the hash table is permitted to become before its bucket array is automatically doubled in size (a resize() operation).

By default, HashMap sets the load factor to 0.75 (75%).

How It Works

Upon instantiation, HashMap allocates a bucket array with an initial capacity (capacity, default 16). The expansion threshold (threshold) is computed as:

\[\text{threshold} = \text{capacity} \times \text{loadFactor} = 16 \times 0.75 = 12\]
import java.util.HashMap;
import java.util.Map;

public class LoadFactorDemo {
    public static void main(String[] args) {
        // Default HashMap: capacity = 16, loadFactor = 0.75 -> threshold = 12
        Map<Integer, String> map = new HashMap<>();

        for (int i = 1; i <= 12; i++) {
            map.put(i, "Value " + i); // Up to 12 elements, no resize occurs
        }

        // Inserting the 13th element (size > threshold):
        // Triggers resize(): capacity doubles to 32, threshold becomes 24
        map.put(13, "Value 13");
    }
}

Real-World Analogy: Imagine a bus with 16 seats. Company policy dictates that as soon as 75% of the seats are occupied (12 passengers), a second bus is dispatched (doubling capacity to 32) before overcrowding occurs at the entrance doors, ensuring passengers can board swiftly without queuing.


🟡 Middle Level

Balancing Memory Footprint and Search Latency

The default loadFactor = 0.75 represents an empirically optimized compromise between memory consumption and algorithmic search latency for get() and put():

Load Factor Setting Memory Consumption Collision Probability Lookup / Insertion Speed
Low (0.50f) High (half the buckets remain empty) Minimal (buckets hold 0 or 1 element) Maximum ($O(1)$ directly on array slot)
Standard (0.75f) Balanced Low (collisions are rare) $O(1)$ average case
High (0.90f - 1.0f) Low (dense bucket packing) High (frequent collision chaining) Slower (iterating linked list chains)
Overloaded (> 1.0f) Table size smaller than element count Guaranteed Collisions Degrades toward linear scan $O(N)$

Customizing loadFactor via Constructors

Java allows configuring the load factor explicitly:

Map<String, String> fastMap = new HashMap<>(64, 0.5f);
Map<String, String> compactMap = new HashMap<>(64, 0.9f);
  • Validation Constraints: The loadFactor must be strictly positive (> 0) and must not be Float.NaN. Otherwise, the constructor throws IllegalArgumentException:
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    

Impact on Full-Table Iteration

The time required to iterate over a HashMap (keySet(), values(), entrySet()) is directly proportional to the sum of the bucket capacity and element count:

\[\text{Time}(\text{Iteration}) \propto \text{capacity} + \text{size}\]

If an excessively low loadFactor is chosen (e.g., 0.1f), the table capacity balloons relative to a modest element count. Iterators are forced to inspect thousands of empty null slots in table[], severely degrading full traversal throughput.


🔴 Senior Level

Mathematical Proof: Poisson Distribution and Average $\lambda \approx 0.5$

The OpenJDK engineers documented the statistical justification for 0.75 in comments within HashMap.java.

Under ideal uniform hashing, the distribution of elements across buckets follows a Poisson distribution:

\[P(k) = \frac{e^{-\lambda} \lambda^k}{k!}\]

where $k$ is the number of elements in a given bucket, and $\lambda$ is the expected average bucket occupancy.

Although the maximum threshold is 0.75, immediately after table doubling the actual load factor drops to 0.375. Over the life of the map, the time-averaged occupancy is approximately 0.5 ($\lambda \approx 0.5$).

Evaluating Poisson probabilities for $\lambda = 0.5$:

  • 0 elements: $60.65\%$ ($e^{-0.5} \approx 0.6065$)
  • 1 element: $30.33\%$
  • 2 elements: $7.58\%$
  • 3 elements: $1.26\%$
  • 4 elements: $0.16\%$
  • 5 elements: $0.016\%$
  • 6 elements: $0.0013\%$
  • 7 elements: $0.000094\%$
  • 8 elements: $\approx 0.00000006$ ($6 \times 10^{-8}$, or 6 in 100 million!)

This mathematical proof explains why the treeification threshold is set to 8: reaching 8 items in a single bucket under normal distribution is statistically impossible. If a bucket reaches 8 nodes, it almost certainly signals a hash algorithm flaw or an active Hash Flooding DoS attack.


Internal Lifecycle of the threshold Field

In HashMap, the load factor is stored as final float loadFactor;:

  1. Lazy Allocation: Calling new HashMap(initialCapacity, loadFactor) does not allocate the backing array. The threshold field is temporarily reused to store the initial capacity calculated via tableSizeFor(initialCapacity):
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
    
  2. First putVal Call: The first write operation invokes resize(). It reads oldThr, allocates a Node<K,V>[] array of size $2^k$, and calculates the true operational threshold:
    newThr = (int)(newCap * loadFactor);
    this.threshold = newThr;
    
  3. MAXIMUM_CAPACITY Boundary: When capacity reaches $2^{30}$ (1 << 30), threshold is set to Integer.MAX_VALUE ($2^{31} - 1$), permanently disabling further resize operations.

Hardware Microarchitecture: CPU Cache Line Locality

Under high load factors (e.g., $0.95$ or $1.2$):

  • Table memory overhead drops, but average collision chain length increases.
  • Iterating linked Node objects involves pointer chasing across scattered heap addresses, triggering frequent L1/L2/L3 CPU Cache Misses. Fetching data from main DRAM requires 60–100 ns versus 1–4 ns from an L1 cache hit.
  • In low-latency architectures (JMH benchmarks), tuning loadFactor = 0.5 can improve read throughput by 30–40% by maximizing direct hits on the contiguous table[] array without dereferencing Node.next pointers.

🎯 Interview Cheat Sheet

1. 30-Second Elevator Pitch

The Load Factor (default 0.75f) is the fill ratio that triggers table doubling: $\text{threshold} = \text{capacity} \times \text{loadFactor}$. It balances memory utilization against lookup latency. Statistical analysis using the Poisson distribution proves that with $\lambda \approx 0.5$, the probability of an 8-node collision chain is negligible ($6 \times 10^{-8}$), establishing the rationale for the treeification threshold of 8. HashMap never shrinks its array when elements are removed.


2. 4 Tricky Interview Questions with Deep Answers

Question 1: Can you pass loadFactor = 2.0f or loadFactor = Float.POSITIVE_INFINITY? What happens?

Answer:

  • loadFactor = 2.0f: Completely valid. The constructor verifies loadFactor > 0 and accepts it. The table will only resize when size > 2 * capacity (e.g., at 33 elements for capacity 16). Each bucket averages 2 entries before resizing.
  • loadFactor = Float.POSITIVE_INFINITY: Valid syntactically (> 0 and not NaN). When calculating newThr = (int)(newCap * loadFactor), floating-point overflow casts to Integer.MAX_VALUE. The table will never resize, remaining locked at its initial capacity and growing infinite collision chains.

Question 2: Why can’t loadFactor be changed on an existing HashMap instance?

Answer: The field is declared final float loadFactor; and initialized exclusively in the constructor. Allowing dynamic modification would require instantaneous threshold recalculation and potentially trigger a massive, blocking resize() operation midway through an arbitrary business operation. To change the load factor, instantiate a new HashMap(capacity, newLoadFactor) and populate it via putAll().

Question 3: How do you size a HashMap to hold $N$ elements without triggering a single resize()?

Answer: The table capacity must satisfy: \(\text{capacity} \times \text{loadFactor} \ge N \implies \text{capacity} \ge \frac{N}{\text{loadFactor}}\) For standard loadFactor = 0.75f, divide by $0.75$ (multiply by $\frac{4}{3}$):

int initialCapacity = (int) Math.ceil(expectedSize / 0.75f);

In Java 19+, use the canonical factory method:

Map<Key, Value> map = HashMap.newHashMap(expectedSize);

which internally executes (int) Math.ceil(expectedSize / 0.75F).

Question 4: Why doesn’t HashMap shrink when entries are deleted via remove()?

Answer:

  1. Preventing Thrashing (Flapping): If HashMap downsized when falling below a threshold, alternating put() and remove() calls on boundary thresholds would cause continuous resizing and re-hashing cycles, devastating throughput.
  2. Allocation Assumptions: Memory allocations assume that if an application reached $N$ entries once, it will likely require that capacity again in subsequent processing loops.

3. Common Pitfalls & Red Flags

  • ❌ “Load factor 0.75 means 75% of buckets contain entries.”
    Correction: It means total map size reaches 75% of total capacity; multiple entries can reside within the same bucket due to collisions.

  • ❌ “Setting load factor to 1.0 is ideal because it uses 100% of memory.”
    Correction: It guarantees long collision chains and severe CPU cache misses, degrading latency.

  • ❌ “Creating a map via new HashMap<>(N) prevents resizing when inserting N elements.”
    Correction: Passing $N$ sets capacity to $N$; threshold becomes $0.75 \times N$. Inserting element $0.75N + 1$ triggers a resize. Always use HashMap.newHashMap(N).

  • ❌ “HashMap shrinks automatically when elements are cleared.”
    Correction: The internal array retains its allocated size until the entire map instance is garbage collected.