Java StringBuilder vs StringBuffer: Efficient Concatenation
StringBuilder is Java's mutable string buffer: appending in a loop is linear, while += on a String is quadratic. Its API, string reversal and StringBuffer.
- Course: Java study plan
- Module: Strings
- Kind: Lesson
- Reading time: 12 min
- Runtime: OpenJDK 18
What is StringBuilder in Java?
StringBuilder in Java is a mutable sequence of characters for building strings piece by piece. Its append, insert, delete and reverse methods change one growable buffer instead of creating a new String each time, so appending n pieces in a loop takes linear time where += on a String takes quadratic time. Call toString() to get the finished string.
Lesson
A String cannot change, so building one incrementally means copying: s += "x" allocates a new string holding all of s plus one character, every time. For ten pieces nobody notices; for a hundred thousand it is the difference between milliseconds and minutes. StringBuilder is the mutable buffer that fixes this — and the class every "reverse a string" and "build the output" answer uses.
The problem, measured
String s = "";
for (int i = 0; i < 100_000; i++) {
s += i; // copies i digits' worth of characters each time
}
Iteration k copies a string of length proportional to k, so the total work is 1 + 2 + … + n ≈ n²/2. At n = 100 000 that is billions of character copies and gigabytes of garbage. The same with a builder:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100_000; i++) {
sb.append(i); // appends into a growable char array
}
String s = sb.toString();
Linear time. The builder keeps a backing array with spare capacity and doubles it when full (amortised O(1) per append).
The API
StringBuilder sb = new StringBuilder(); // capacity 16
StringBuilder sb = new StringBuilder(1024); // pre-sized when you know roughly
StringBuilder sb = new StringBuilder("seed");
sb.append("text"); // overloaded: String, char, int, long, double, boolean, Object, char[], CharSequence
sb.append(42).append(' ').append(3.5); // returns this — chain freely
sb.insert(0, "start "); // insert at an index
sb.replace(0, 5, "begin"); // replace [begin, end) with a string
sb.delete(0, 6); // remove [begin, end)
sb.deleteCharAt(sb.length() - 1); // drop the last char — e.g. a trailing comma
sb.setCharAt(0, 'B'); // mutate one char
sb.setLength(0); // clear (keeps capacity) — reuse the builder
sb.setLength(3); // truncate
sb.reverse(); // in place
sb.length(); sb.charAt(i); sb.indexOf("x"); sb.substring(1, 3); // like String
sb.toString(); // the String — copies the used part of the buffer
sb.capacity(); // current buffer size (rarely needed)
append(char) versus append(String): sb.append('a') and sb.append("a") both work; the char form avoids creating a string. sb.append(c + 1) where c is a char appends a number (promotion!) — write sb.append((char) (c + 1)).
The idioms
Build output once, print once (from Module 1):
StringBuilder out = new StringBuilder();
for (int i = 0; i < n; i++) out.append(values[i]).append('\n');
System.out.print(out);
Join with a separator, without a trailing one:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < items.length; i++) {
if (i > 0) sb.append(", ");
sb.append(items[i]);
}
// or: String.join(", ", items) / StringJoiner
Reverse:
String reversed = new StringBuilder(s).reverse().toString();
reverse() handles surrogate pairs correctly (an emoji stays an emoji), which a hand-written char loop does not.
Palindrome check without allocating: two indices walking inward on the string itself — no builder needed. Reach for a builder when you produce text, not when you inspect it.
Build a string with a computed length — repeat and String.valueOf(char[]) are alternatives:
String dashes = "-".repeat(40);
char[] row = new char[n]; Arrays.fill(row, '*'); String stars = new String(row);
StringBuilder vs StringBuffer
StringBuffer is the Java 1.0 original with every method synchronized. StringBuilder (Java 5) is the same API without the locks and is what you use. The synchronisation in StringBuffer is nearly useless anyway — thread safety of individual appends does not make a sequence of appends atomic. The interview answer: "same API; StringBuffer is synchronised and slower; use StringBuilder unless a builder is genuinely shared across threads, which it should not be."
StringJoiner and String.join
StringJoiner sj = new StringJoiner(", ", "[", "]");
sj.add("a").add("b");
sj.toString(); // "[a, b]"
sj.setEmptyValue("(none)"); // what toString gives with no elements
String.join("/", parts); // a StringJoiner underneath
list.stream().collect(Collectors.joining(", ")); // the stream form (Module 15)
When + is fine
The compiler turns "x=" + x + ", y=" + y into a single efficient concatenation (an invokedynamic call since Java 9; a StringBuilder chain before). A builder is not faster for one expression — it is faster for loops and for conditional building. Rules of thumb: + for a single expression; StringBuilder when appending in a loop or across several statements; String.join/Collectors.joining for joining a collection.
Equality on builders
StringBuilder a = new StringBuilder("x"), b = new StringBuilder("x");
a.equals(b) // false — StringBuilder does NOT override equals (identity)
a.toString().equals(b.toString()) // true
a.compareTo(b) // 0 — Comparable since Java 11
"x".contentEquals(a) // true
A StringBuilder as a map key or in a set is a bug: identity-based, and mutable.
Capacity and memory
The builder grows by (old + 1) * 2 when full. If you know the final size — building a fixed-width table, encoding a known-length message — pass it to the constructor and avoid the regrowth copies. After toString(), the builder still holds its buffer; drop the reference or setLength(0) to reuse.
Interview angle
- "Why is
+=in a loop slow?" Each concatenation copies the whole accumulated string: O(n²) total. - "
StringBuildervsStringBuffer?" Same API; buffer is synchronised; builder is faster and the default. - "How do you reverse a string?"
new StringBuilder(s).reverse().toString(); mention the two-pointerchar[]alternative and surrogate pairs if pressed. - "Does
StringBuilder.equalscompare contents?" No — identity. UsecompareToortoString().equals.
Key takeaways
Stringconcatenation in a loop is quadratic;StringBuilder.appendis amortised linear.appendreturnsthis— chain;setLength(0)clears;reverse()is surrogate-safe.+in a single expression is already efficient — the builder is for loops and conditional assembly.StringBufferis the synchronised, slower ancestor;StringBuilderis the one to use.- Builders compare by identity; convert to
Stringforequalsand as map keys.
Common questions
What is the difference between StringBuilder and StringBuffer?
They have the same API, but every StringBuffer method is synchronized, which makes it slower, while StringBuilder, added in Java 5, has no locks. Use StringBuilder; the per-call locking of StringBuffer does not make a sequence of appends atomic anyway.
Why is string concatenation in a loop slow in Java?
Each s += x copies the whole accumulated string into a new one, because strings are immutable, so n appends copy on the order of n squared characters. A StringBuilder appends into one buffer that doubles when full, which is linear overall. A single a + b + c expression is already efficient.
How do I reverse a string in Java?
Use new StringBuilder(s).reverse().toString(). reverse() keeps surrogate pairs in order, so an emoji stays intact, which a hand-written loop swapping char values does not. In an interview, also mention the two-pointer swap over a char[].
Does StringBuilder.equals compare contents?
No. StringBuilder does not override equals, so it compares identity, and two builders holding the same text are not equal. Compare a.toString().equals(b.toString()), use compareTo, available since Java 11, or call "text".contentEquals(builder).
How do I clear a StringBuilder in Java?
Call sb.setLength(0). It empties the builder but keeps its buffer, so the builder can be reused without allocating again. sb.delete(0, sb.length()) also works, and creating a new StringBuilder is fine when reuse does not matter.
Exercises
Run-length encoding
Read one line and print its run-length encoding: each maximal run of the same character becomes the character followed by the run length. Build the result with a StringBuilder.
Input: one non-empty line of letters. Output: one line.
Example: aaabccdddd → a3b1c2d4
Devowel, then mirror
Read one line of words separated by single spaces. Print two lines: first the sentence with every vowel (aeiouAEIOU) removed — build it with a StringBuilder, appending only consonants; second the original sentence with each word reversed in place (word order unchanged) using new StringBuilder(word).reverse(). Words that lose all their letters disappear from the first line (collapse the spaces).
Example: Java is fun →
Jv s fn
avaJ si nufIn this module: Strings
← The String API you will actually use · Formatting output and text blocks →