W3docs

Java StringBuilder

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

A 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 numbers in the output (capacity=32 at the start, capacity=66 at the end). Every append in the chain fits inside the initial capacity of 32 — the built string is only 24 characters. The insert and replace then push the length to 40, which overflows 32 and triggers exactly one grow (to 2 × 32 + 2 = 66). Pre-sizing closer to the true final length — new StringBuilder(48) here — would have avoided that single reallocation entirely. That is the whole game: the better your capacity estimate, the fewer copy-on-grow events.

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?
Why is `StringBuilder` typically preferred over `String` concatenation in a loop?
Was this page helpful?