Command Palette

Search for a command to run...

PHASE 6Intermediate Java 5+ ~31 min· topic 3 of 8

Topic 6.3

StringBuilder and StringBuffer

In one line

StringBuilder is a mutable, growable buffer of characters for building text step by step without creating a new String on every change; call toString() once at the end. StringBuffer is the older, synchronized twin, rarely needed today.

Think of it like this

Writing a shopping list. With immutable Strings you'd have to copy the whole list onto a fresh sheet of paper every time you add one item: tea (new sheet), tea and samosa (new sheet), tea, samosa and cake (new sheet). A StringBuilder is a notepad: you keep writing on the same page, cross things out, insert a line, and only at the end tear off a clean copy (toString()) to hand over.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Mutable
Can be changed after it's created. A StringBuilder is mutable; a String is not.
Buffer
A block of memory used to collect data before it's used, like a notepad for characters.
Capacity
How many characters the builder's internal array can hold before it must grow. It's at least as big as length().
Amortised cost
The average cost per operation over a long run, counting the occasional expensive step (like regrowing) spread across all the cheap ones.
Method chaining
Calling one method after another on the same object in one expression, possible because each method returns that object.
synchronized
A Java keyword that lets only one thread at a time run a piece of code on the same object (Phase 13).
Thread
One independent path of execution inside a program. Several threads can run at the same time (Topic 13.1).

Step by step

01The cost of + in a loop

String s = ""; for (...) s = s + i; looks harmless. But Strings are immutable (Topic 6.1), so each + builds a new String containing all old characters plus the new ones. Pass 1 copies 1 character, pass 2 copies 2, pass n copies n. Total: about n²/2 copies, and every old String becomes garbage.

Since Java 9, javac compiles a + b + c into one invokedynamic call that sizes the result once, which makes a single expression fast. It can't help a loop: each pass is still a separate expression creating a separate String.

The cost of + in a loopdiagram
Rendering diagram…

02Creating a builder and appending

new StringBuilder() starts empty with capacity 16. new StringBuilder("tea") starts with that text and capacity 3 + 16. new StringBuilder(1000) starts empty with room for 1000 characters, useful when you know roughly how big the result will be.

append is overloaded for every type: String, char, int, long, double, boolean, Object (calls String.valueOf, so null becomes the text null), char[] and any CharSequence. Each returns this.

Main.javawhole filejava
StringBuilder sb = new StringBuilder();
sb.append("Total: ").append(3).append(" items, paid=").append(true);
String result = sb.toString();   // "Total: 3 items, paid=true"

03Editing in the middle

insert(i, x) shifts everything from index i right and writes x there. delete(start, end) removes [start, end) and shifts the tail left. replace(start, end, str) does both. These are O(n) because characters after the edit point move, just like inserting into the middle of an ArrayList (Topic 9.2).

Indexes are checked: insert(10, "x") on a 3-character builder throws StringIndexOutOfBoundsException. delete is forgiving about end: an end past the length is clamped to the length.

reverse() reverses the characters, and it's Unicode-aware: a surrogate pair (an emoji, Topic 6.8) stays in the right order instead of being split.

04How the internal array grows

Inside, a builder (via AbstractStringBuilder) has a byte[] value, a coder (Latin-1 or UTF-16, the same compact-strings idea as String) and an int count of used characters. length() is count; capacity() is the array size.

When an append needs more room than the capacity, the builder grows to about 2 * old + 2 (or exactly what's needed, if that's larger), allocates the new array, copies the old one in, and continues. Doubling is why appends are amortised O(1): the big copies happen rarely.

setLength(n) cuts (or pads with '\u0000') the text without shrinking the array. trimToSize() shrinks the array to fit, which only matters for builders you keep around for a long time.

How the internal array growsdiagram
Rendering diagram…

05toString() makes an independent copy

toString() creates a new String from the used part of the array. Later changes to the builder don't touch that String. That's what lets the builder be mutable while everything it produced stays immutable.

Because each call copies, don't call toString() inside a loop just to check something; use length(), charAt() or indexOf() on the builder directly.

06StringBuffer: the synchronized twin

StringBuffer and StringBuilder share the parent AbstractStringBuilder and have the same methods. The difference is that StringBuffer's methods are synchronized: a thread must take the buffer's lock before each call (Topic 13.3).

Even so, a sequence like if (buf.length() > 0) buf.append(",") isn't atomic across the two calls, so StringBuffer rarely gives real thread safety for compound logic. The modern approach is to give each thread its own StringBuilder and combine results afterwards.

07Joining: String.join and StringJoiner

Building a, b, c with a loop means handling the separator: either skip it before the first item or cut the last one off with setLength(sb.length() - 2).

String.join(", ", items) (Java 8) does it in one call for arrays and any Iterable. StringJoiner adds a prefix, a suffix and a value to use when nothing was added. Collectors.joining (Topic 10.6) is the stream version; all three use a StringBuilder inside.

Main.javawhole filejava
StringJoiner j = new StringJoiner(", ", "[", "]");
j.add("tea").add("samosa");
System.out.println(j);   // [tea, samosa]

Try it yourself

  1. 1

    Predict the capacity

    In the capacity example, after the 17th character, append 18 more characters ("abcdefghijklmnopqr"). Predict the new capacity using 2 * old + 2, then run and check.

  2. 2

    Reverse a sentence's words

    Using only a StringBuilder and String.split(" "), turn "chai is hot" into "hot is chai". Predict where you need to add the spaces, then run.

  3. 3

    Feel the n² cost

    Copy the timing sketch into a Main class and run it with n at 10,000, then 100,000. The += time should grow about 100 times for 10 times more items, while the builder grows about 10 times. That's what O(n²) vs O(n) looks like.

Code & diagrams

Editing a StringBuilder in place Java 5+ New tab
Sign in to run this example in your browser.

Expected output

appended:  tea and samosa!2
inserted:  Order: tea and samosa!2
replaced:  Order: chai and samosa!2
deleted:   Order: chai!
setCharAt: order: chai!
reversed:  !iahc :redro
length: 12, indexOf("iahc"): 1
append returns the same builder: true
Length vs capacity: watching the array grow Java 5+ New tab

The growth formula is an implementation detail of current JDKs; only the amortised O(1) behaviour is promised.

Sign in to run this example in your browser.

Expected output

empty: length 0, capacity 16
16 chars: length 16, capacity 16
17 chars: length 17, capacity 34
setLength(5): length 5, capacity 34
trimToSize: length 5, capacity 5
text now: 01234
new StringBuilder("hello"): length 5, capacity 21
new StringBuilder(100): length 0, capacity 100
Joining with separators, and builder equality Java 11+ New tab

StringBuilder.compareTo arrived in Java 11; String.join and StringJoiner in Java 8.

Sign in to run this example in your browser.

Expected output

builder:     tea, samosa, cake
String.join: tea, samosa, cake
join a List: a | b | c
StringJoiner: [tea, samosa, cake]
empty joiner: nothing ordered
x.equals(y): false
x.compareTo(y) == 0: true
contentEquals: true
Loop concatenation vs StringBuilder (timing sketch, not runnable)java
int n = 100_000;

long t0 = System.nanoTime();
String s = "";
for (int i = 0; i < n; i++) {
    s += i;                       // new String every pass: O(n^2) copying
}
long t1 = System.nanoTime();

StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
    sb.append(i);                 // amortised O(1) per append
}
String fast = sb.toString();
long t2 = System.nanoTime();

System.out.println("+=      : " + (t1 - t0) / 1_000_000 + " ms");
System.out.println("builder : " + (t2 - t1) / 1_000_000 + " ms");
// Typical laptop: "+=" takes seconds, the builder a few milliseconds.
// Use JMH (Topic 14.6) for trustworthy numbers.
StringBuffer when threads really share one buffer (fragment)java
StringBuffer log = new StringBuffer();      // every method is synchronized

Runnable task = () -> {
    for (int i = 0; i < 10_000; i++) {
        log.append('x');                    // safe: one thread at a time per call
    }
};
Thread a = new Thread(task);
Thread b = new Thread(task);
a.start(); b.start();
a.join();  b.join();
System.out.println(log.length());           // always 20000

// With StringBuilder the length can come out short, or append can even throw
// ArrayIndexOutOfBoundsException, because two threads grow the array at once.

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Assign a literal to a StringBuilder

Write StringBuilder sb = "hello"; and then String s = sb;.

terminal
$ javac Main.java
── what you'll see ──
Main.java:3: error: incompatible types: String cannot be converted to StringBuilder
StringBuilder sb = "hello";
^
Main.java:4: error: incompatible types: StringBuilder cannot be converted to String
String s = sb;
^
2 errors

Break #2

Insert past the end

Call new StringBuilder("abc").insert(10, "x").

terminal
$ javac Main.java && java Main
── what you'll see ──
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: Range [10, 3) out of bounds for length 3
at java.base/jdk.internal.util.Preconditions$1.apply(Preconditions.java:55)
...
at java.base/java.lang.StringBuilder.insert(StringBuilder.java:336)
at Main.main(Main.java:4)

Break #3

Use a StringBuilder as a map key

Put new StringBuilder("tea") into a HashSet, then check set.contains(new StringBuilder("tea")).

terminal
$ java Main.java
── what you'll see ──
false

Myth vs fact

Myth

The compiler turns every + into a StringBuilder, so + in loops is fine.

Fact

The compiler optimises each single expression. In a loop, every pass is a new expression that creates a new String and copies everything so far, which is O(n²) overall.

Myth

StringBuffer is the thread-safe choice, so use it to be safe.

Fact

Its per-call locking costs time and doesn't make multi-call logic atomic. Build text per thread with StringBuilder; share results through proper concurrent tools (Phase 13).

Myth

Always use StringBuilder instead of +.

Fact

For a one-line expression like "Hi " + name + "!", + is just as fast (Java 9+ sizes it in one go) and easier to read. Reach for a builder when you build text across a loop or many statements.

Myth

toString() hands out the builder's internal array.

Fact

It copies the characters into a new String, so the String stays immutable when the builder changes later.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Java 9's indy string concatenation (JEP 280) compiles a + b + c to invokedynamic bootstrapped by StringConcatFactory.makeConcatWithConstants. The strategy computes the exact length and fills one array, often beating a hand-written StringBuilder for single expressions. Compile with -XDstringConcat=inline to get the old StringBuilder chains.

  • ▸

    Pre-sizing (new StringBuilder(expectedLength)) avoids repeated grow-and-copy steps. A builder that ever held a non-Latin-1 character switches its whole array to UTF-16 (inflate), doubling its memory, and doesn't switch back.

  • ▸

    The JIT's escape analysis can sometimes eliminate builder allocation entirely for short-lived local builders, and HotSpot has had special -XX:+OptimizeStringConcat handling for StringBuilder.append().toString() chains for years.

  • ▸

    StringBuilder implements Comparable<StringBuilder> since Java 11, but still not equals. CharSequence.compare(a, b) (Java 11) compares any two CharSequences by content.

Remember this

  1. 1

    A **StringBuilder** (in java.lang, added in Java 5) holds a mutable sequence of characters in an internal array that has spare room, its capacity. append writes into that spare room, so adding text usually costs only the characters copied, not a whole new object. When the room runs out, the builder allocates a bigger array (roughly double) and copies the old contents across, so a long series of appends costs amortised O(1) per character.

  2. 2

    Most methods change the builder in place and return the same builder, so calls chain: sb.append("tea").append(' ').append(3). The main ones: append(anything), insert(index, x), delete(start, end), deleteCharAt(i), replace(start, end, str), setCharAt(i, c), reverse(), setLength(n), plus read methods length(), charAt(i), indexOf(str) and substring(...). Ranges are start inclusive, end exclusive, like String.substring.

  3. 3

    toString() copies the current characters into a new, immutable String. The builder stays usable: change it again and call toString() again for another String. Earlier Strings never change, because they're separate copies.

  4. 4

    Why bother? s = s + x inside a loop creates a brand-new String each pass and copies everything built so far into it: for n pieces that's about n²/2 character copies and n throwaway objects. A builder copies each character about once (plus occasional regrowth). For 100,000 appends the difference is milliseconds versus many seconds.

  5. 5

    **StringBuffer** (Java 1.0) has the same methods, but every one is synchronized, so it's safe for several threads to call at once. That locking costs time, and sharing one text buffer between threads is rare, so StringBuilder replaced it for everyday use. You'll still meet StringBuffer in old code and a few old APIs (like Matcher.appendReplacement before Java 9).

  6. 6

    Builders don't override equals or hashCode: two builders with the same text are not equals (they use identity, from Object). Compare contents with sb1.compareTo(sb2) == 0 (Java 11), sb.toString().equals(...) or "text".contentEquals(sb), and never use a builder as a HashMap key. For joining with separators, String.join and StringJoiner (Java 8) are often clearer than a hand-written loop.

Explain it without notes

01

Why is s = s + x in a loop slow, and how does StringBuilder fix it?

02

What's the difference between length() and capacity(), and how does the capacity grow?

03

When would you use StringBuffer instead of StringBuilder?

04

Why can't you use equals to compare two StringBuilders?

05

Is "Hello, " + name + "!" slower than using a StringBuilder?

Practice

01

Write static String repeatWithDash(String word, int times) that returns e.g. tea-tea-tea for ("tea", 3), using a StringBuilder. Print the results for 3 and 1.

02

Write static boolean isPalindrome(String s) using StringBuilder.reverse(), ignoring case. Test "Level", "chai" and "Racecar".

03

Build a multiplication table line for 7 (7 x 1 = 7 up to 7 x 5 = 35), one per line, in a single StringBuilder, and print it once.

Trade-offs

  • ↔

    StringBuilder is fast for building text step by step, but it's mutable: never share one across threads or store one as a key; convert to a String when the text is finished.

  • ↔

    + is clearest for single expressions and is optimised since Java 9; a builder adds noise there. Use the builder for loops and multi-step assembly.

  • ↔

    Pre-sizing a builder saves copies but wastes memory if you guess far too high. Size it when you know the order of magnitude, otherwise let it grow.

Done when you can

  • Done when you can explain why += in a loop is O(n²) and a builder is O(n).

  • Done when you can use append, insert, delete, replace, reverse and setLength confidently.

  • Done when you can explain length vs capacity and how the array grows.

  • Done when you know why builders aren't equals and how to compare them.

  • Done when you can choose between StringBuilder, StringBuffer, String.join and StringJoiner.