skip to content

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%

answer

  1. ArrayList = Object[] + size + capacity
  2. Grow = ~1.5x: allocate, System.arraycopy, drop old
  3. ~30 resizes for 1M from default cap 10
  4. Total copies ≈ 2–3× n; ~30 garbage arrays
  5. Pre-size → one allocation, zero resizes

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.

solid answer

~50 s

An ArrayList wraps an Object[] with a capacity separate from its size. When you add to a full list, ArrayList grows: it computes a new capacity (old + old/2, i.e. ~1.5x), allocates a new array, copies the existing elements with System.arraycopy, swaps it in, and lets the old array become garbage. Starting from the default capacity of 10 and adding a million elements triggers on the order of ~30 reallocations, copying a growing prefix each time — totaling roughly 2-3 million element-reference copies plus ~30 throwaway arrays for the GC. Each individual add is still amortized O(1), but the aggregate copying is pure overhead you incurred only because the list didn't know its final size. Pre-sizing with new ArrayList<>(1_000_000) allocates the backing array once and eliminates every intermediate resize and its garbage. The same idea applies to StringBuilder and other array-backed buffers on hot paths.

code

java · 12 lines
java
int n = 1_000_000;

// Default capacity (10): triggers ~30 grow-and-copy resizes
List<Integer> grows = new ArrayList<>();
for (int i = 0; i < n; i++) grows.add(i);

// Pre-sized: one allocation, zero resizes
List<Integer> presized = new ArrayList<>(n);
for (int i = 0; i < n; i++) presized.add(i);

// Copying from a known source sizes itself automatically
List<Integer> copy = new ArrayList<>(presized);

go deeper

for a junior

Can state that a full ArrayList makes a bigger array and copies elements over, and that pre-sizing avoids repeating that.

for a middle

Describes the ~1.5x growth formula, System.arraycopy, the garbage produced, and estimates the resize count for a large list.

for a senior

Explains amortized O(1) vs avoidable constant-factor/GC overhead, contrasts ArrayList growth with HashMap doubling+rehash, and knows over-sizing/trimToSize trade-offs.

for a principal

Treats it as one tool in a constant-factor/allocation-pressure budget, decides where it's worth the readability cost, and reasons about GC pause impact on latency SLAs.

## ArrayList internals An **ArrayList** is a resizable list backed by a single `Object[] elementData`. It tracks two numbers: - **size** — how many elements you've added (what `size()` returns), - **capacity** — the length of `elementData` (not exposed publicly). The default initial capacity is **10** (technically the array is allocated lazily on the first add). Reads and appends index directly into the array, giving O(1) access and amortized-O(1) append. ## The grow-and-copy cycle When you call `add` and `size == capacity` (the array is full), ArrayList must grow: 1. **Compute new capacity:** `newCapacity = oldCapacity + (oldCapacity >> 1)` — i.e. old plus half of old, **≈ 1.5x**. (From the default of 10, capacities go 10 → 15 → 22 → 33 → 49 → …) 2. **Allocate** a new `Object[]` of that length. 3. **Copy** all `size` existing references into it via `System.arraycopy` (a fast bulk memory copy, but still O(size)). 4. **Replace** the field with the new array; the old array becomes **garbage**. 5. Place the new element and increment size. Note it copies **references**, not deep copies of the objects — so the per-element cost is small, but it scales with the current size and happens repeatedly. ## Quantifying the cost Growth by a constant factor (1.5x) means the number of resizes for `n` elements is **logarithmic** — about `log_1.5(n / 10)`. For n = 1,000,000 that's roughly **30 resizes**. The total number of elements copied across all resizes is a geometric series that sums to roughly **2–3× n** (a few million reference copies for a million-element list). You also create ~30 intermediate arrays that immediately become garbage — extra work for the **garbage collector** (the JVM subsystem that reclaims unused memory). ### Why this is 'amortized O(1)' yet still worth avoiding **Amortized O(1)** means: averaged over all `n` appends, each costs a constant amount, because the expensive resizes get geometrically rarer. The *total* work is O(n) — you can't beat linear. So no individual add is slow, and you won't get an asymptotic blow-up. **But** that O(n) of copying is *avoidable overhead* when you already know `n`: it produces nothing you keep, and it generates GC garbage that can cause pauses on latency-sensitive paths. ## Pre-sizing If you know (or can bound) the final size, allocate it once: ```java List<Foo> list = new ArrayList<>(expectedSize); ``` Now appends never trigger a resize (assuming the estimate holds): one allocation, zero copies, zero intermediate garbage. If you're copying from a known source, `new ArrayList<>(sourceCollection)` sizes itself automatically. ## Related buffers The same principle applies to other array-backed structures: - **StringBuilder** grows its `char[]` similarly; `new StringBuilder(expectedLength)` avoids reallocations when concatenating many pieces. - **HashMap/HashSet** have an analogous but distinct concern (load factor + rehash — see the dedicated question). - Raw arrays/buffers on hot paths: allocate to the known size once rather than growing. ## Caveats - **Over-sizing** wastes memory (a large, partly-empty array) and hurts cache locality. ArrayList never auto-shrinks; call `trimToSize()` if you over-allocated and want the slack back. - **Unknown size:** if you genuinely can't estimate, default growth is fine — it's already efficient. Don't guess wildly. - This is a **micro-optimization**: apply it where size is known and the path is hot; don't litter every list with capacity arguments.

  • If you don't know the exact size but can bound it, what's a reasonable strategy?
    Pre-size to a sensible upper estimate (or a typical-case size) to remove most resizes, accepting a little wasted memory. If the over-estimate could be large and memory matters, you can trimToSize() once the list is fully built. The goal is to cut the common-case resize count, not to be exact.
  • Does pre-sizing help with iteration or random-access speed?
    Not directly — ArrayList already gives O(1) random access and contiguous iteration regardless of how it was sized. Pre-sizing only affects build-time cost (fewer allocations/copies) and can marginally improve cache locality by avoiding a final array that grew larger than needed.

saying these in an interview costs you the question

  • Saying ArrayList doubles capacity (that's HashMap; ArrayList is ~1.5x)
  • Thinking a resize deep-copies the stored objects rather than copying references
  • Claiming pre-sizing improves asymptotic complexity (it stays O(n); it removes constant-factor/GC overhead)
  • Forgetting that an over-sized ArrayList doesn't shrink on its own

context