skip to content

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%

answer

  1. Single expression: compiler fuses it (one builder pre-9; invokedynamic + StringConcatFactory in 9+, JEP 280)
  2. Loop: separate statements, each depends on prior result → can't fuse → O(n^2)
  3. invokedynamic = bootstrap chooses strategy at first run, sizes once
  4. JIT inlines but won't remove per-iteration allocation
  5. Rule: trust + for fixed expressions, StringBuilder for loops

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.

solid answer

~50 s

A String concatenation *expression* compiles to a single fused operation. Before Java 9 the compiler emitted explicit StringBuilder.append calls for it; since Java 9 (JEP 280) it emits one invokedynamic instruction bootstrapped by StringConcatFactory, letting the runtime pick an optimal strategy and even pre-size the result in one shot. Either way, the whole expression is handled together, so `"a" + b + c` is efficient. The loop case is different: `s += piece` across iterations is a sequence of *separate statements*, each reading the result of the previous iteration and reassigning the variable. The compiler can only fuse concatenations it sees together in one expression; it cannot hoist a builder across the loop because that would change object identity and semantics it can't prove safe. So each iteration still allocates a fresh String — O(n^2). The takeaway: trust the compiler for fixed, single-expression concatenations; reach for StringBuilder yourself for loops.

go deeper

for a junior

Knows plain + outside a loop is fine and not to use += in loops; doesn't need the bytecode detail.

for a middle

Can say the compiler optimizes a single concatenation expression but not loop accumulation, and explain the loop is a series of separate dependent statements.

for a senior

Explains pre-9 StringBuilder lowering vs Java 9+ invokedynamic/StringConcatFactory (JEP 280) and precisely why cross-iteration fusion is impossible for the compiler.

for a principal

Discusses why indified concat is a maintainability win (strategy evolves in the JDK), bytecode/decompilation implications, and JIT limits; sets guidance distinguishing expression vs loop concatenation.

## Two different things people call 'concatenation' 1. A **single concatenation expression** evaluated once, e.g. `String msg = "Hello, " + name + "!";` — the `+`s all live in one expression. 2. **Repeated concatenation across loop iterations**, e.g. `s += part;` run n times — many *separate statements*, each depending on the last. These compile very differently, and conflating them is the root of most confusion. ## How the compiler handles a single expression The Java `+` on Strings is **not** a runtime String method; it is a language operator the *compiler* lowers (translates) into bytecode. - **Before Java 9 (Java 8 and earlier):** for an expression like `a + b + c`, `javac` generated bytecode that created **one** `StringBuilder`, appended `a`, `b`, `c`, and called `toString()`. So even old Java already optimized a *single* expression into a single builder — you didn't pay for `"ab"` then `"abc"` intermediate Strings. - **Java 9+ (JEP 280, 'Indified String Concatenation'):** instead of hard-coding the StringBuilder dance, `javac` emits a single **`invokedynamic`** instruction. *invokedynamic* is a JVM bytecode that defers, until first run, *how* a call is actually performed; the first time it executes, a **bootstrap method** wires up the real implementation. For string concat the bootstrap lives in **`java.lang.invoke.StringConcatFactory`** (the `makeConcatWithConstants` bootstrap). The runtime then synthesizes an optimized concatenation routine — it can compute the exact final length, allocate the result array **once**, and write all parts in directly, with no StringBuilder and no intermediate Strings at all. Benefits of the invokedynamic approach: the strategy can be improved in the JDK without recompiling your code, and it typically produces *less* garbage and faster concatenation than the old explicit-StringBuilder pattern. Either way, the important point is the same: **the compiler optimizes the whole expression as a unit.** ## Why the loop can't be fused Now the loop: ```java String s = ""; for (int i = 0; i < n; i++) { s += parts[i]; // a separate statement each iteration } ``` Each iteration is its **own** statement: `s = s + parts[i];`. The compiler's concatenation optimization works **within one expression**. Here: - The right-hand side reads the *current* value of `s` (the result the previous iteration produced) and produces a new String that is then assigned back to `s`. - Between iterations, `s` is an observable, immutable String reference. Other code (or even just the loop's own next read) depends on it being a real String. To fuse this into a single builder, the compiler would have to recognize the loop pattern, prove that `s` isn't observed in some way that requires its intermediate immutable identity, and rewrite control flow across iterations — a transformation `javac` does **not** perform (and the JIT does not do this particular rewrite either; it can inline the per-iteration concat, but it won't eliminate the per-iteration allocation). So each iteration still does a full immutable-String concatenation, re-copying everything → the classic **O(n^2)**. ## The practical rule - **Single expression, fixed number of pieces** (even with a few variables): just write `a + b + c`. The compiler fuses it; it's clear and fast. Manually using StringBuilder there is needless noise. - **Accumulating across a loop:** you must introduce the StringBuilder yourself (or use `String.join` / `Collectors.joining`), because the compiler will not span iterations for you. ## A subtlety worth knowing Because Java 9 routes single-expression concat through `invokedynamic`/`StringConcatFactory` rather than a literal StringBuilder, decompiling/inspecting bytecode shows `makeConcatWithConstants` rather than `StringBuilder.append` — surprising if you learned the pre-9 model. The observable behavior (efficient single-expression concat) is the same or better; only the mechanism changed.

  • What bytecode instruction and bootstrap does Java 9+ use for a String concatenation expression?
    A single invokedynamic instruction bootstrapped by java.lang.invoke.StringConcatFactory (makeConcatWithConstants), which lets the runtime synthesize an optimized, single-allocation concatenation.
  • Does the JIT compiler rescue a += loop at runtime?
    No. The JIT can inline the per-iteration concatenation logic, but it does not rewrite the loop to reuse a single buffer, so each iteration still allocates and the loop stays O(n^2). You must use StringBuilder yourself.

saying these in an interview costs you the question

  • Claiming the compiler turns loop += into a single StringBuilder automatically — it only fuses within one expression, not across iterations.
  • Asserting Java still always uses StringBuilder for + — since Java 9 single-expression concat uses invokedynamic/StringConcatFactory, not literal StringBuilder bytecode.
  • Saying the JIT eliminates the per-iteration String allocations in the loop — it can inline but does not perform that fusion.
  • Believing pre-Java-9 made `a+b+c` create intermediate Strings — even Java 8 used one StringBuilder for a single expression.

context