skip to content

How does a StringBuilder grow its internal buffer, and when would you pre-size its capacity?

level: seniorimportance: should knowfreq 55%

answer

  1. default capacity 16
  2. grow = 2*old + 2, copy over
  3. doubling -> amortized O(1)
  4. length() vs capacity()
  5. pre-size constructor / ensureCapacity in hot paths

basics

~20 s

A StringBuilder wraps a char array with a default capacity of 16. When you append past capacity it allocates a bigger array (about double plus two) and copies everything over. If you know roughly the final size, pass it to the constructor so it avoids those reallocations.

solid answer

~40 s

StringBuilder keeps a backing `char[]` whose length is its capacity (default 16, or 16 plus the initial string's length). `append` writes into the unused tail; when the next write would exceed capacity it grows by allocating a new array of size `oldCapacity * 2 + 2` (or exactly the needed length if that is still larger) and copying the existing characters. Each growth is O(current length), but because capacity doubles, growths are rare and the amortized cost per character stays O(1) — total building is O(n). If you can estimate the final length, pass it via `new StringBuilder(estimatedSize)` (or call `ensureCapacity`); this skips the intermediate reallocations and array copies, reducing both CPU and garbage. `length()` is the live character count; `capacity()` is the allocated array size and is usually larger.

go deeper

for a junior

Knows there is an internal array with a starting size and that it gets bigger when you append a lot.

for a middle

Knows the default capacity is 16 and that the builder reallocates when full; can pass an initial capacity to the constructor.

for a senior

Explains the 2*old+2 doubling rule, the amortized-O(1) analysis, length vs capacity, and pre-sizes deliberately in hot paths.

for a principal

Connects it to allocation/GC behavior at scale, profiling to find real string-building hot spots, and the general amortized-doubling pattern shared with other growable collections.

## What 'capacity' means StringBuilder stores characters in a backing **`char[]`** (a fixed-length array of characters). Two numbers describe it: - **length** — how many characters are actually in use right now (`length()`). - **capacity** — how many characters the current array could hold before it must be replaced (`capacity()`). capacity is always >= length. The array is an implementation detail; you interact through methods, but understanding it explains the performance. ## Default capacity `new StringBuilder()` starts with capacity **16**. `new StringBuilder("abc")` starts with capacity 16 + 3 = 19 (the string's length plus 16 headroom). `new StringBuilder(100)` starts with capacity exactly 100. ## How growth works When an `append` (or `insert`) needs more room than the current capacity, StringBuilder must **resize**: it cannot enlarge an existing array (Java arrays are fixed-length), so it: 1. Allocates a new, larger `char[]`. 2. Copies all existing characters into it. 3. Switches to the new array; the old one becomes garbage. The new size is computed as **`oldCapacity * 2 + 2`**, unless the data being added needs even more, in which case it uses exactly the required length. The `+2` is a small historical fudge; the key idea is **doubling**. ## Why doubling gives amortized O(1) A single resize copies all current characters, costing O(length). That sounds expensive, but doubling means resizes happen only at capacities 16, 34, 70, 142, ... — exponentially spaced. Across building a string of n characters, the total copying work is bounded by roughly 2n (a geometric series: n + n/2 + n/4 + ... < 2n). Spread over n appends, that is **constant amortized cost per character**, so building the whole thing is O(n). This is the same amortized-doubling argument that makes ArrayList efficient. ## When and how to pre-size If you know (even approximately) the final length, tell the builder up front: ```java // Without pre-sizing: grows 16 -> 34 -> 70 -> ... several copies StringBuilder sb = new StringBuilder(); // With pre-sizing: one allocation, zero intermediate copies StringBuilder sb = new StringBuilder(expectedLength); ``` or expand an existing one: `sb.ensureCapacity(expectedLength)`. This matters in **hot paths** that build large strings repeatedly — fewer allocations, less array copying, less garbage for the collector. For small or occasional strings the default is perfectly fine; pre-sizing there is premature optimization. ## Useful related methods - `length()` — current character count; `setLength(n)` can truncate or zero-pad. - `capacity()` — current array size (rarely needed in app code). - `trimToSize()` — shrinks the array to the current length, reclaiming slack (occasionally useful for a long-lived builder). ## Summary - Backing `char[]`, default capacity 16; length <= capacity. - Overflow triggers allocate-new-array-of-`2*old+2` then copy. - Doubling => amortized O(1) per append, O(n) total. - Pre-size via the int constructor / `ensureCapacity` when the size is known and the path is hot.

  • Why is the amortized cost of append O(1) despite resizing being O(n)?
    Because capacity doubles, resizes are exponentially rare and total copying across n appends sums to under 2n (a geometric series). Averaged over all appends that is constant per append.
  • What does new StringBuilder("hello") set the capacity to?
    16 plus the string length, so 16 + 5 = 21. The default 16-char headroom is added on top of the initial content.

saying these in an interview costs you the question

  • Saying the array grows by a fixed +1 or +16 each time (it doubles).
  • Claiming each append is O(n) — it is amortized O(1).
  • Confusing length() (chars used) with capacity() (array size).
  • Pre-sizing every builder reflexively, even for tiny strings (premature optimization).

context