Why is using + to build a string inside a loop a performance pitfall, and what should you use instead?
answer
- Loop + = new String each pass, copies all chars so far
- Total work ~ n^2 (quadratic), O(n) garbage
- invokedynamic optimizes one expression, not across iterations
- Fix: StringBuilder.append in loop, toString() once -> O(n)
- Pre-size builder; StringBuffer = synchronized, rarely needed
basics
~20 sEach + in a loop makes a whole new string and copies all the characters so far. Over many iterations that copying gets very expensive. Use a StringBuilder and append in the loop, then call toString() once at the end.
solid answer
~40 sThe problem is that string concatenation per expression is fine, but a loop runs the expression once per iteration, and each iteration produces a brand-new String by copying every character accumulated so far. Building an n-character result that way copies on the order of n-squared characters total, so it degrades quadratically as the string grows — fine for tiny loops, disastrous for large ones. The compiler's invokedynamic/StringConcatFactory optimization helps a single expression but cannot merge separate iterations, so it does not save you here. The fix is to use a mutable StringBuilder: create it once before the loop, append inside, and call toString() once afterward. That amortizes to linear time because the buffer grows geometrically and characters are not recopied each pass. StringBuffer is the synchronized (and slower) equivalent, rarely needed.
go deeper
Knows to use StringBuilder instead of + when looping, even if not the exact reason.
Explains the immutability-driven per-iteration copy and that StringBuilder.append avoids it.
Quantifies the quadratic vs linear behavior, knows pre-sizing and the StringBuffer distinction, and that JEP 280 does not rescue the loop.
Sets team guidance (when + is fine vs builder vs String.join), and reasons about GC/allocation pressure and capacity tuning at scale.
## Recap: why a single + is cheap but a loop of + is not Strings are **immutable**: `+` cannot extend an existing string, it must create a **new** one holding all the characters. For a single expression that is one allocation. But put `+` in a loop: ```java String s = ""; for (int i = 0; i < n; i++) { s = s + part; // a fresh String every iteration } ``` Each iteration is a *separate* concatenation expression. Iteration `i` must copy the `~i` characters already in `s` into a brand-new string, then add `part`. Copying `1 + 2 + 3 + ... + n` characters totals about `n²/2` character copies — **quadratic** (O(n²)) work and O(n) throwaway objects. For `n = 10` it is invisible; for `n = 100000` it can take seconds and thrash the garbage collector. ## Why the modern compiler optimization does NOT save you JEP 280's `invokedynamic`/`StringConcatFactory` optimizes the codegen of **one** concatenation expression (it can size and fill a single buffer once). But the loop executes that expression `n` separate times; the compiler cannot see across iterations to fuse them. So even on the newest JDK, `s = s + part` in a loop stays quadratic. ## The fix: StringBuilder `StringBuilder` is a **mutable** char buffer. You append to the same buffer repeatedly without recopying the whole thing each time, then materialize the final `String` once: ```java StringBuilder sb = new StringBuilder(); // optionally new StringBuilder(expectedSize) for (int i = 0; i < n; i++) { sb.append(part); } String s = sb.toString(); // single final allocation ``` The internal array grows **geometrically** (roughly doubles when full), so total copying is amortized **linear** (O(n)). Pre-sizing with `new StringBuilder(capacity)` when you can estimate the length avoids even the resize copies. ## StringBuilder vs StringBuffer `StringBuffer` is the older, **synchronized** (thread-safe) sibling. Its method-level locking costs performance and is almost never needed because a builder is typically local to one thread. Default to `StringBuilder`; reach for `StringBuffer` only if a single buffer is genuinely shared across threads (rare — usually a design smell). ## When + is still fine A fixed, small number of pieces in **one** expression (`a + ", " + b + "!"`) is perfectly fine and more readable than a builder — the compiler already does it in essentially one shot. The pitfall is specifically **accumulation across iterations**. ## Alternatives for joining collections For joining a collection with a separator, prefer purpose-built tools: `String.join(", ", list)`, a `Collectors.joining(", ")` stream collector, or `StringJoiner`. They use a builder internally and read clearly. ## Rule of thumb - One expression, few parts: use `+`. - Accumulating in a loop or unknown count: use `StringBuilder` (pre-size if possible). - Joining a collection: use `String.join` / `Collectors.joining` / `StringJoiner`.
- The compiler turns + into StringBuilder/invokedynamic anyway — so why is a loop still slow?Because the optimization applies per expression, and a loop runs the concatenation expression once per iteration. The compiler cannot fuse separate iterations into one builder, so each pass still allocates and copies all prior characters — quadratic overall.
- What is the difference between StringBuilder and StringBuffer?They share the same API, but StringBuffer is synchronized (thread-safe) and therefore slower; StringBuilder is unsynchronized and the default choice. Use StringBuffer only when one buffer is shared across threads, which is rare.
saying these in an interview costs you the question
- Believing JEP 280 makes + in a loop efficient
- Claiming + and StringBuilder have the same complexity in a loop
- Reaching for StringBuffer by default when no threading is involved
- Replacing a single small + expression with a builder for no reason