W3docs

Java StringBuilder

Build mutable strings efficiently in Java with the StringBuilder class — append, insert, reverse, and more.

Java StringBuilder

String is immutable; growing one with += in a loop is quadratic. StringBuilder is the standard library's answer: a single object with a resizable internal buffer that you append, insert, delete, and reverse in place, then convert to an immutable String once at the end. It's the workhorse behind every modern JDK string-building pattern, and the right reach the moment you find yourself accumulating text in a loop or across method calls.

Constructing a builder

StringBuilder sb = new StringBuilder();             // empty, capacity 16
StringBuilder withCap = new StringBuilder(1024);    // empty, preallocated capacity
StringBuilder fromText = new StringBuilder("hi ");  // capacity = length + 16

The no-arg constructor starts at capacity 16, which is fine for short results and a pessimistic choice for long ones. If you know roughly how big the final string will be, pass the capacity up front — every avoided grow is one fewer array allocation and one fewer arraycopy. new StringBuilder(estimatedLength) is the single most effective microoptimisation in this whole part of the book.

Chaining: every mutator returns this

Every mutating method on StringBuilder returns the builder itself, so calls compose into a single expression:

String greeting = new StringBuilder()
    .append("Hello, ")
    .append(name)
    .append('!')
    .append('\n')
    .toString();

This is convention, not magic; you could break it across multiple statements with no functional difference. The chained style mirrors how the compiler rewrites + chains under the hood, which is itself the reason the chained form reads naturally in Java.

The mutator family

StringBuilder has a small, focused surface:

  • append — adds text or any primitive at the end. Overloaded for every primitive, char[], CharSequence, and Object (calls toString).
  • insert(offset, ...) — same overloads, but at an arbitrary position.
  • delete(start, end), deleteCharAt(i) — remove a range or a single character.
  • replace(start, end, replacement) — replace a range with a new substring; lengths can differ.
  • reverse() — flip the buffer in place.
  • setCharAt(i, ch) — rewrite a single character.
  • setLength(n) — truncate (or pad with ) to n characters.

These edit the buffer; they do not return a new String. To get a snapshot of the contents as an immutable string, call toString().

Inspecting and converting

  • length() — current character count.
  • capacity() — current size of the internal array. Always ≥ length().
  • charAt(i), substring(start) / substring(start, end) — read access, identical to String.
  • indexOf(s), lastIndexOf(s) — locate a substring.
  • toString() — produce an immutable String. Call this once at the end; calling it repeatedly during a build is wasted allocation.
  • ensureCapacity(n) — pre-grow the buffer to at least n.
  • trimToSize() — shrink the buffer to fit the current contents. Rarely needed.

How the buffer grows

Internally, StringBuilder holds a byte[] (or char[] on older JDKs). When an append would overflow, the buffer is reallocated to roughly 2 × oldCapacity + 2, and the old contents are copied across. Each grow is O(n) in the current size, but the doubling pattern makes the total cost of n appends O(n) amortised — quite unlike repeated String concatenation, where the same loop is O(n²).

append "a" — capacity 16, length 1
... 15 more — capacity 16, length 16
append "b" — grow to 34, length 17
... 17 more — capacity 34, length 34
append "c" — grow to 70, length 35

If you know the final length, you skip every one of those reallocations by constructing with that capacity directly.

StringBuilder vs the + operator

For short, statically-known string assembly, the compiler does the right thing on its own. "Hello, " + name + "!" is rewritten at compile time into either a single StringBuilder chain or a call to StringConcatFactory.makeConcatWithConstants (Java 9+). Both are efficient. You don't need to micro-manage these expressions.

The pattern to avoid is += inside a loop over an unknown count:

// O(n²) — every += allocates a new String holding everything seen so far
String out = "";
for (String token : tokens) {
  out += token + "|";
}

// O(n) — one buffer, one final String
StringBuilder sb = new StringBuilder();
for (String token : tokens) {
  sb.append(token).append('|');
}
String out = sb.toString();

If the loop runs a handful of times, the difference is invisible. At a few thousand tokens, it's a benchmark-able problem. At a million, the first form is a hang and the second is milliseconds.

StringBuilder is not thread-safe

StringBuilder deliberately omits synchronization to be fast in the overwhelmingly common single-threaded case. If two threads append to the same builder simultaneously, the results are undefined: lost writes, overwritten characters, or an ArrayIndexOutOfBoundsException from the grow path. For the rare case where one builder is shared across threads, use the synchronised twin — StringBuffer — instead. In practice you almost never share a builder; each thread builds its own.

A worked example

The program below uses every part of the surface that matters in everyday code: chained append, an explicit insert, a replace that changes a range's length, a reverse, and one final toString. The capacity is printed at the start and end to make the buffer doubling visible.

java— editable, runs on the server

Watch the capacity number. With a starting capacity of 32, every append in the chain fits — no reallocation. The insert and replace shift characters around but don't change the length enough to trip a grow either. That's what pre-sizing buys you: predictable allocation and no copy-on-grow surprises.

What's next

StringBuilder is fast because it's not thread-safe. Its synchronised twin is the right answer in the (rare) case where you genuinely want one buffer shared between threads — same API, methods that lock. Continue to Java StringBuffer.

Practice

Practice

Why is `StringBuilder` typically preferred over `String` concatenation in a loop?