skip to content

Collection Pre-Sizing

Sizing an ArrayList or HashMap up front avoids repeated array copies and rehashing, which is why the expectedSize / 0.75 + 1 capacity rule exists. Interviewers use it to check you understand what amortized O(1) hides.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

How do you correctly size a HashMap to hold a known number of entries without triggering a resize, and why isn't passing the entry count directly enough?

level: middleimportance: must knowfreq 60%

answer

  1. Constructor arg = bucket capacity, not entry count
  2. Resize threshold = capacity × load factor (0.75)
  3. capacity = n / 0.75 + 1
  4. Resize = double table + rehash every entry (O(n))
  5. JDK 19+: HashMap.newHashMap(n) / HashSet.newHashSet(n)

basics

~20 s

Pass capacity = expectedSize / 0.75 + 1, not the raw count. A HashMap resizes when it's about 75% full (the load factor), so if you pass the exact count it will still grow once you near it. Java 19+ has HashMap.newHashMap(n) that does this for you.

solid answer

~40 s

A HashMap's constructor argument is the initial bucket-array capacity, not the number of entries it can hold. The map automatically resizes (doubles its bucket array and rehashes every entry) once the number of entries exceeds capacity × load factor, where the default load factor is 0.75. So if you call new HashMap<>(100) and insert 100 entries, it resizes around entry 76 — defeating the point. To hold n entries without a resize you size for headroom: capacity = (int)(n / 0.75) + 1. HashMap also rounds capacity up to the next power of two internally. From Java 19, HashMap.newHashMap(n), Set, and Map factory helpers do this calculation, so prefer them. Pre-sizing avoids the doubling-and-rehash cost, which is more expensive than ArrayList's copy because every key's bucket must be recomputed.

code

java · 12 lines
java
// Sizing a HashMap for a known number of entries
int expected = 1000;

// Wrong: capacity is bucket count, not entry count → resizes ~entry 750
Map<String, Integer> wrong = new HashMap<>(expected);

// Right (manual rule): account for the 0.75 load factor
int capacity = (int) (expected / 0.75f) + 1;
Map<String, Integer> right = new HashMap<>(capacity);

// Best (JDK 19+): factory takes the expected element count directly
Map<String, Integer> best = HashMap.newHashMap(expected);

go deeper

for a junior

Knows a HashMap has an initial capacity argument and that giving it a size can avoid growth; may not know the load-factor adjustment.

for a middle

Correctly applies capacity = n/0.75 + 1, explains the load factor and the double-plus-rehash resize, and knows HashSet shares the behavior.

for a senior

Articulates why rehash is costlier than an array copy, mentions power-of-two rounding and HashMap.newHashMap(n), and reasons about when the optimization is worthwhile.

for a principal

Weighs load-factor tuning trade-offs (memory vs collision rate) at a system level, standardizes the factory helpers across a codebase, and ties sizing to measured allocation/GC behavior under load.

## What HashMap stores and how lookups work A **HashMap** maps keys to values. Internally it holds an array of **buckets** (slots), `Node[] table`. To find where a key goes, it computes the key's **hash code**, spreads the bits, and reduces it to an index `hash & (table.length - 1)` — which is why the table length is always a **power of two** (that masking trick only works then). Each bucket holds the entries whose keys landed on that index (a short linked list, or a balanced tree once a bucket gets large). Good distribution means most buckets hold 0–1 entries, giving **average O(1)** `get`/`put`. ## The load factor and why HashMap resizes If you keep adding entries into a fixed number of buckets, buckets get crowded, collisions rise, and lookups degrade toward O(n). To keep buckets sparse, HashMap tracks a **threshold = capacity × load factor**. The **load factor** is the maximum fullness ratio before growing; the default is **0.75** (75%). When `size` exceeds the threshold, HashMap **resizes**: 1. Allocates a new table **double** the length, 2. **Rehashes** — recomputes each existing entry's index for the new length and moves it. Rehashing is O(n) and touches every entry, making it costlier than an `ArrayList` copy (which just moves references). Each resize also discards the old table (GC pressure). ## Why passing the raw count fails Because the threshold is `capacity × 0.75`, a HashMap created with capacity `n` can only hold ~`0.75 × n` entries before it grows. So: ``` new HashMap<>(100); // threshold ≈ 75 (actually 96×0.75 after power-of-two rounding) // insert 100 entries → resizes at least once ``` The constructor argument is **bucket capacity**, not **entry capacity** — a classic confusion. ## The correct sizing rule To guarantee no resize while inserting `n` entries, you need `capacity × 0.75 > n`, i.e. `capacity > n / 0.75`. The conventional, safe formula is: ```java int capacity = (int) (expectedSize / 0.75f) + 1; Map<K, V> map = new HashMap<>(capacity); ``` The `+ 1` covers the strict-inequality / rounding edge. HashMap then rounds `capacity` **up to the next power of two** internally (e.g. you ask for 134, it uses 256). So the result is always at least enough. ### Java 19+ shortcut Manually doing this math is error-prone, so JDK 19 added factory helpers that take the **expected element count** and apply the formula for you: ```java Map<K, V> map = HashMap.newHashMap(expectedSize); Set<E> set = HashSet.newHashSet(expectedSize); ``` Prefer these when available — they encode the rule correctly and read clearly. (`HashSet` is backed by a `HashMap`, so the same load-factor logic and the same sizing concern apply to it.) ## Common misuse and edge cases - **Copy constructor:** `new HashMap<>(otherMap)` already sizes itself for the source's entries — you don't redo the math. - **Over-sizing:** a too-large capacity wastes memory (a big, mostly-empty bucket array) and slightly hurts cache locality, but never breaks correctness. - **Custom load factor:** you can pass a second constructor arg; higher (e.g. 0.9) saves memory but raises collisions, lower (e.g. 0.5) speeds lookups at memory cost. The default 0.75 is the tuned compromise — change it only with measurement. ## Bottom line Size a `HashMap`/`HashSet` for **headroom**, not the raw count: `n / 0.75 + 1`, or just `HashMap.newHashMap(n)` on modern JDKs. This avoids the double-and-rehash work entirely on known-large builds.

  • What is the cost difference between a HashMap resize and an ArrayList resize?
    Both allocate a bigger backing array and discard the old one. But ArrayList only block-copies element references (System.arraycopy). HashMap must rehash — recompute each entry's bucket index for the new, larger table and relink it — touching every entry individually. So a HashMap resize is generally more expensive per element than an ArrayList grow.
  • Does the same sizing rule apply to HashSet?
    Yes. HashSet is implemented on top of a HashMap (elements are keys), so it has the same 0.75 load factor and the same resize behavior. Use the same n/0.75+1 capacity, or HashSet.newHashSet(n) on JDK 19+.

saying these in an interview costs you the question

  • Passing the expected entry count directly as the capacity
  • Thinking the load factor is the same as initial capacity
  • Believing HashMap rehash just copies references like ArrayList (it recomputes every bucket index)
  • Assuming capacity is used as-is rather than rounded up to a power of two

context

open as a page

What does it mean to 'pre-size' a Java collection like ArrayList or HashMap, and why might you do it when you already know roughly how many elements you'll add?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Pre-sizing means telling a collection up front how many items you expect, via a constructor argument (e.g. new ArrayList<>(1000)). It avoids the collection having to grow and copy its internal array repeatedly as you add items, which saves time and memory churn.

open as a page

Walk through what happens when an ArrayList outgrows its backing array, and quantify the cost of building a large list from the default capacity versus pre-sizing.

level: middleimportance: should knowfreq 40%

basics

~20 s

When an ArrayList's backing array is full and you add one more element, it creates a new array about 1.5x larger, copies all existing elements over, and discards the old array. Building a big list from the default capacity (10) triggers many such grow-and-copy cycles; pre-sizing does the allocation once.

open as a page

When is pre-sizing collections a premature or counterproductive optimization, and how would you decide where it's actually worth applying?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Pre-sizing only helps when the final size is known and the code is hot. If sizes are small, the path is cold, or the size is unknown, the constructor argument adds clutter without measurable benefit — and a bad over-estimate wastes memory. Measure first; apply it on proven hot, known-size build paths.

open as a page

When you build collections via the Streams API (collect, toList, groupingBy), can you still benefit from pre-sizing, and what are the limits of doing so?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Streams usually can't pre-size their result because the element count isn't known until the stream finishes — collectors append into a default-capacity collection that grows as usual. If you know the size and it's a hot path, build the collection manually with a pre-sized constructor, or use a collector supplier that creates a pre-sized container.

open as a page