skip to content

Why do we use StringBuilder or StringBuffer to build strings instead of repeatedly concatenating with the + operator in a loop?

level: juniorimportance: must knowfreq 78%

answer

  1. String immutable -> + makes a new object
  2. loop concat = O(n^2) + garbage
  3. StringBuilder = one growing char[]
  4. append, then toString() once
  5. single-line + is auto-optimized

basics

~20 s

Java Strings are immutable, so each + creates a brand-new String object. In a loop that copies the text over and over, which is slow and wasteful. StringBuilder edits one mutable buffer in place, so it is much faster.

solid answer

~40 s

In Java a String is immutable: once created its characters never change. So `s = s + x` does not modify `s`; it allocates a new String, copies the old contents plus the new piece, and reassigns the reference. Inside a loop of n iterations that is roughly O(n^2) work and lots of garbage for the GC. StringBuilder (and the older StringBuffer) keeps a single mutable `char[]` buffer; `append` writes into that buffer, growing it occasionally, giving amortized O(n) overall. You build everything up, then call `toString()` once to produce the final immutable String. Default to StringBuilder in single-threaded code; reach for StringBuffer only when multiple threads share the same instance.

go deeper

for a junior

Knows String is immutable and that building strings in a loop with + is slow, so a StringBuilder should be used and toString() called at the end.

for a middle

Can explain the O(n^2) vs O(n) difference, the garbage created, and that a single-line a+b+c is already optimized by the compiler.

for a senior

Discusses the internal char[] doubling growth, amortized analysis, GC pressure, and when the JIT/compiler does or does not fuse concatenations.

for a principal

Frames it as an allocation/locality concern at scale, can reason about pre-sizing capacity, escape analysis, and where String concatenation hot spots actually matter versus premature optimization.

## The problem: String is immutable A **String** in Java is an object that holds a fixed sequence of characters. **Immutable** means that after a String is created, its contents can never be changed. Every method that looks like it modifies a String (`toUpperCase`, `substring`, `replace`, `+`) actually returns a *new* String and leaves the original untouched. So this line: ```java String s = "a"; s = s + "b"; ``` does NOT change the object `"a"`. It creates a new String `"ab"`, then makes the variable `s` point at it. The old `"a"` is now garbage. ## Why concatenation in a loop is slow Consider building a string of n pieces: ```java String result = ""; for (int i = 0; i < n; i++) { result = result + piece; // new String every iteration } ``` On iteration i, `result` already holds about i characters. `result + piece` must allocate a new array, copy all i existing characters, then append the new piece. Copying i characters on every iteration means total work of 1 + 2 + 3 + ... + n, which is proportional to **n squared** (written O(n^2)). It also produces n throwaway String objects, pressuring the **garbage collector** (the part of the JVM that reclaims unused memory). Note: a single expression like `a + b + c` on *one* line is fine — the compiler turns it into one StringBuilder behind the scenes. The danger is concatenation **inside a loop**, where each iteration is a separate statement the compiler cannot fuse. ## The fix: a mutable buffer **StringBuilder** holds an internal, resizable `char[]` (an array of characters) plus a count of how many slots are used. `append(...)` writes the new characters into the free space at the end. When the array fills up, StringBuilder allocates a larger array (typically doubling) and copies once. Because doubling is rare, the *amortized* (averaged-over-all-appends) cost per character is constant, so building the whole string is **O(n)** total. ```java StringBuilder sb = new StringBuilder(); for (int i = 0; i < n; i++) { sb.append(piece); } String result = sb.toString(); // one immutable String, built once ``` You mutate the builder as much as you like, then call `toString()` once to snapshot it into a real immutable String. ## StringBuilder vs StringBuffer (preview) Both do exactly the same buffer trick. **StringBuffer** (from Java 1.0) makes its methods `synchronized`, meaning thread-safe but with locking overhead. **StringBuilder** (added in Java 5) is the same API without synchronization — faster, and the right default for normal single-threaded code. Pick StringBuffer only when one builder instance is genuinely shared and mutated across threads. ## Takeaways - String is immutable; `+` allocates and copies. - Loop concatenation is O(n^2) and garbage-heavy. - StringBuilder mutates one growing buffer: O(n), one final `toString()`. - Single-line `a + b + c` is already optimized — only loops need a builder.

  • Is `String r = a + b + c;` on one line a performance problem?
    No. The compiler converts a single concatenation expression into one StringBuilder/append/toString sequence, so it is already efficient. The pathological case is concatenation inside a loop, where each iteration is a separate statement.
  • Why does StringBuilder give O(n) instead of O(n^2)?
    It writes into one buffer and only resizes occasionally (doubling). Doubling means total copying is bounded by ~2n characters across all growths, so the amortized cost per append is constant.

saying these in an interview costs you the question

  • Claiming String is mutable, or that + edits the existing object.
  • Saying every use of + is bad — a single a+b+c line is already compiled to one StringBuilder.
  • Thinking StringBuilder is itself immutable; it is the mutable counterpart.
  • Forgetting the final toString() and trying to use the StringBuilder where a String is required.

context