skip to content

StringBuilder vs StringBuffer

Both build strings mutably; StringBuffer synchronizes every method and StringBuilder does not, which is why StringBuilder is the default. Interviewers ask for the difference and expect you to point out that the synchronization is rarely useful.

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

questions

5

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

open as a page

What is the core difference between StringBuilder and StringBuffer, and which should you reach for by default?

level: middleimportance: must knowfreq 85%

basics

~20 s

They have the same API and do the same job: build strings in a mutable buffer. StringBuffer's methods are synchronized (thread-safe but slower); StringBuilder's are not (faster). Use StringBuilder by default; use StringBuffer only when one instance is shared across threads.

open as a page

How does StringBuilder's fluent method chaining work, and what does a method like append or reverse return?

level: juniorimportance: should knowfreq 48%

basics

~10 s

Mutating methods like append, insert, and reverse change the builder in place and then return the same builder object (this). Because they return the builder, you can chain calls one after another, like sb.append("a").append("b").reverse().

open as a page

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

level: seniorimportance: should knowfreq 55%

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.

open as a page

A teammate proposes using StringBuffer as a shared field so several worker threads can append log lines to it concurrently. Critique this design.

level: principalimportance: should knowfreq 40%

basics

~20 s

StringBuffer keeps each append from corrupting the buffer, but it does not control the order, makes every thread fight over one lock, and you still need extra synchronization to read it safely. A shared mutable builder is a bottleneck and a smell; give each thread its own builder or use a proper concurrent structure.

open as a page