skip to content

String Concatenation in Loops

Concatenating with += in a loop is O(n^2) because each step copies the whole char array of an immutable String; a pre-sized StringBuilder makes it linear. Interviewers follow up on why plain + is fine outside a loop, thanks to invokedynamic string concatenation.

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

questions

5

Why is building a String by repeatedly using += inside a loop slow, and how slow does it get as the number of iterations grows?

level: juniorimportance: must knowfreq 70%

answer

  1. String is immutable → += copies everything each time
  2. 1+2+...+n = n^2/2 copies → O(n^2)
  3. StringBuilder = mutable buffer, append is amortized O(1) → O(n)
  4. Garbage: a new throwaway String per iteration
  5. Plain + outside a loop is fine

basics

~20 s

Strings in Java can't be changed, so each += makes a brand-new String by copying all the characters so far plus the new ones. Doing that every loop turn means you copy more and more text, so the total work grows much faster than the number of loops.

solid answer

~40 s

Java Strings are immutable, so += can't append in place; it creates a new String each time by copying the existing characters plus the new ones. On iteration i you copy roughly i characters, so summing over n iterations gives 1+2+...+n, which is about n^2/2 character copies — O(n^2) total work and lots of throwaway objects for the garbage collector. For small n it's invisible, but at thousands or millions of iterations it dominates runtime. The fix is StringBuilder, which keeps a growable char[] buffer and appends in amortized O(1), making the whole loop O(n). So inside loops use StringBuilder (or a stream collector / String.join); plain + outside loops is fine.

go deeper

for a junior

Knows String is immutable and that += in a loop is slow, and reaches for StringBuilder. Can state 'a new string is created each time'.

for a middle

Quantifies it as O(n^2) vs O(n), explains the re-copy of the whole accumulated array each iteration, and the extra GC pressure from throwaway objects.

for a senior

Derives the n(n+1)/2 series, contrasts amortized O(1) append, and knows plain + outside loops (and single-expression concat) is fine and even compiler-optimized.

for a principal

Frames it as an algorithmic-complexity smell to catch in review/profiling, weighs StringBuilder vs streams/String.join, and considers pre-sizing and allocation/GC impact at scale.

## The setup A **String** in Java is an object that holds a sequence of characters. The critical property is that it is **immutable**: once a String object exists, its contents can never be changed. There is no method that edits a String in place; every operation that 'changes' a String actually returns a *new* String object. The `+` operator on Strings is **concatenation** — gluing two strings together. `"ab" + "cd"` produces a new String `"abcd"`. The `+=` form (`s += x`) is just shorthand for `s = s + x`: it reassigns the variable `s` to point at a newly created String. ## What happens inside the loop Consider: ```java String s = ""; for (int i = 0; i < n; i++) { s += "x"; // s = s + "x"; } ``` Because String is immutable, on each iteration the JVM must: 1. Allocate a new character array big enough for the *current* length of `s` plus the new piece. 2. **Copy every character already in `s`** into that new array. 3. Copy the new piece on the end. 4. Wrap it in a new String object and point `s` at it. The old String becomes garbage. The key cost is step 2: it copies the *whole* accumulated string every time. ## Why that is O(n^2) Let's count character copies. On iteration `i`, the string already holds about `i` characters, so step 2 copies about `i` characters. Total copies over the whole loop: ``` 1 + 2 + 3 + ... + n = n(n+1)/2 ≈ n^2 / 2 ``` That sum (an arithmetic series) is proportional to **n^2** — we call this **O(n^2)**, or *quadratic*. 'O(...)' (Big-O) is a way of describing how the work grows as input size grows, ignoring constant factors. Quadratic means: double the iterations and you roughly *quadruple* the work. Concrete feel: 1,000 iterations ≈ 500,000 copies (fine). 1,000,000 iterations ≈ 500,000,000,000 copies — that can take many seconds or minutes, plus it churns out ~1,000,000 throwaway String objects and their backing arrays, hammering the **garbage collector** (the JVM subsystem that reclaims unused objects). ## The fix: StringBuilder `StringBuilder` is a *mutable* companion to String. It holds an internal, growable `char[]` buffer and a length. `append(...)` writes into the free space at the end — no full re-copy — so each append is **amortized O(1)** (occasionally the buffer is full and is grown by copying once, but spread across all appends the average cost per append is constant). The whole loop becomes **O(n)** (linear): ```java StringBuilder sb = new StringBuilder(); for (int i = 0; i < n; i++) { sb.append("x"); } String result = sb.toString(); ``` You build the final String once, at the end, with `toString()`. ## When plain + is fine Outside a loop, a *fixed* number of concatenations like `"Hello, " + name + "!"` is totally fine — it's O(1) work and, as a bonus, since Java 9 the compiler turns a single concatenation expression into one efficient call (via the `StringConcatFactory`), so you don't even pay for intermediate Strings. The quadratic trap is specifically **repeated += across many loop iterations**, which the compiler cannot fuse because each iteration is a separate statement that reads the previous result.

  • Roughly how many total character copies happen if you += a single char n times into an initially empty string?
    About n(n+1)/2 ≈ n^2/2 — the arithmetic series 1+2+...+n, because iteration i copies the ~i characters already accumulated.
  • Why doesn't the compiler just rewrite the loop to use a StringBuilder for me?
    It can fuse the concatenations within a single expression/statement, but across loop iterations each += is a separate statement that reads the previous loop's result, so it can't safely merge them into one builder. You must do it yourself.

saying these in an interview costs you the question

  • Saying += 'just appends to the existing string' — it cannot; String is immutable and a new object is created each time.
  • Claiming the JIT/compiler optimizes the loop into a StringBuilder automatically — it does NOT fuse concatenation across separate loop iterations.
  • Confusing O(n^2) with O(n): the slowdown is the re-copy of the whole accumulated string, not just the new piece.
  • Thinking the cost is only allocation; the dominant cost is copying all prior characters every iteration.

context

open as a page

How do you efficiently build a large String inside a loop, and what role does pre-sizing the StringBuilder's capacity play?

level: middleimportance: must knowfreq 60%

basics

~20 s

Use a StringBuilder: create one, call append in the loop, then toString at the end. If you know roughly how big the result will be, give the StringBuilder that size up front so it doesn't have to keep growing and copying its internal buffer.

open as a page

When is using the plain + operator for String concatenation perfectly acceptable, and how do you decide between +, StringBuilder, and String.format on a given line of code?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Plain + is fine whenever you join a fixed, small number of strings in one expression and not inside a loop — like building a log message or a greeting. Use StringBuilder when you build up a string across loop iterations. String.format is for readable templated text when speed isn't critical.

open as a page

Since Java 9, how does the compiler handle the + operator on Strings, and why can it optimize a single concatenation expression but not concatenation spread across loop iterations?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The compiler turns one concatenation expression (like a + b + c) into a single efficient operation, so plain + outside loops is fast. But in a loop, each += is its own separate step that depends on the previous turn's result, so the compiler can't merge them into one builder for you.

open as a page

You profile a service and find a hot method spending most of its time in String operations while building a large CSV-like payload in a loop. How do you diagnose and fix it, and what trade-offs guide your choice of approach?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Look for += String building inside the loop — that's almost certainly the culprit, because it copies everything each turn (O(n^2)). Replace it with a StringBuilder (append in the loop, toString once), pre-size it if you can estimate the length, and re-profile to confirm the time and garbage dropped.

open as a page