StringBuilder & StringBuffer
StringBuilder and StringBuffer are mutable character-sequence classes built for efficiently constructing and editing text in place, sidestepping String's immutability overhead.
Where String is immutable and every apparent modification allocates a new object, StringBuilder (and its older, synchronized cousin StringBuffer) maintain a genuinely mutable, resizable internal character buffer. Appending, inserting, deleting, or reversing text mutates that buffer in place — no new object is created per operation — which makes these classes the right tool whenever you're assembling or repeatedly editing text, especially inside loops.
Mutable Character Sequences
Both StringBuilder and StringBuffer implement the CharSequence interface and represent a sequence of characters backed by a resizable internal array. Unlike String, calling a mutating method like append changes the object itself and returns a reference to that same object (enabling method chaining), rather than producing a separate new object.
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(", ");
sb.append("World!");
System.out.println(sb.toString()); // "Hello, World!"
System.out.println(sb.length()); // 13Core Methods: append, insert, delete, replace, reverse
StringBuilder's API covers the common editing operations you'd want on a growing piece of text, all mutating in place and (except toString/charAt/etc.) returning this for fluent chaining.
delete(start, end) and replace(start, end, str) both use a half-open [start, end) range, just like String.substring.
StringBuilder sb = new StringBuilder("Hello World");
sb.append("!"); // "Hello World!"
sb.insert(5, ","); // "Hello, World!"
sb.delete(0, 6); // " World!" (removes indices [0,6))
sb.replace(0, 1, ""); // "World!" (replaces range [0,1) with "")
sb.reverse(); // "!dlroW"
System.out.println(sb); // "!dlroW" — StringBuilder overrides toString(), so println works directly
StringBuilder chained = new StringBuilder();
chained.append("a").append("b").append("c"); // method chaining
System.out.println(chained); // "abc"Other Frequently Used Methods
Beyond the core editing operations, StringBuilder supports indexed access, searching, and length manipulation similar to String, plus setCharAt for in-place single-character replacement.
StringBuilder sb = new StringBuilder("banana");
System.out.println(sb.charAt(0)); // 'b'
System.out.println(sb.indexOf("nan")); // 2
sb.setCharAt(0, 'B');
System.out.println(sb); // "Banana"
sb.setLength(3); // truncates in place
System.out.println(sb); // "Ban"
sb.deleteCharAt(0);
System.out.println(sb); // "an"Capacity
A StringBuilder maintains an internal buffer with a capacity that may exceed the current character length, so appends can be satisfied without reallocating on every call. When the buffer fills up, it typically grows automatically (a common HotSpot strategy roughly doubles capacity, though this exact growth policy is implementation behavior, not a language guarantee). You can pre-size the buffer for performance when you have a good estimate of the final length.
Pre-sizing with new StringBuilder(int) is purely a performance hint; it never limits how large the buffer can eventually grow.
StringBuilder small = new StringBuilder(); // default initial capacity (commonly 16)
StringBuilder sized = new StringBuilder(1000); // pre-sized capacity, avoids repeated growth
System.out.println(small.capacity()); // 16 (typical default — implementation detail)
System.out.println(sized.capacity()); // 1000
StringBuilder fromText = new StringBuilder("hi"); // capacity is typically text length + 16
System.out.println(fromText.capacity()); // 18 (typical — implementation detail)StringBuilder vs. String — Why Mutability Matters
Because String is immutable, building up text through repeated + concatenation in a loop typically allocates and discards a new String object on every iteration — for large loops this is measurably slower and creates unnecessary garbage-collector pressure. StringBuilder sidesteps this by mutating one buffer in place across the whole loop, then converting to a String exactly once at the end via toString().
// Less efficient for large n — many intermediate String objects
String slow = "";
for (int i = 0; i < 10000; i++) {
slow += i;
}
// Efficient — one mutable buffer for the whole loop
StringBuilder fast = new StringBuilder();
for (int i = 0; i < 10000; i++) {
fast.append(i);
}
String result = fast.toString(); // convert to an immutable String exactly onceStringBuilder vs. StringBuffer
StringBuffer predates StringBuilder (it existed since Java 1.0; StringBuilder was added in Java 5) and has essentially the same API. The single meaningful difference: StringBuffer's methods are synchronized, making it safe to share across multiple threads without external synchronization, at the cost of that synchronization overhead on every call — even in single-threaded code where it's pure overhead with no benefit.
Even StringBuffer's synchronization only makes individual method calls atomic — a sequence of several calls (e.g., checking length then appending) is still not atomic as a whole, so StringBuffer alone doesn't automatically make compound multi-step operations thread-safe.
StringBuilder StringBuffer
-------------------------------------- --------------------------------------
NOT synchronized (not thread-safe) synchronized methods (thread-safe)
Faster in single-threaded code Slower due to synchronization overhead
Preferred choice in modern code Legacy — use mainly for old/shared-state code
Java 5+ Java 1.0+Practical Usage
Reach for StringBuilder whenever you're building a string incrementally: formatting output in a loop, constructing SQL/CSV/JSON text piece by piece, implementing a custom toString() that assembles many fields, or reversing/editing text in place. String.join, text blocks, and String.format are often better for simple, one-shot formatting where no loop is involved.
static String toCsvRow(String[] fields) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < fields.length; i++) {
if (i > 0) sb.append(',');
sb.append(fields[i]);
}
return sb.toString();
}Edge Cases
StringBuilder overrides toString() but does NOT override equals() or hashCode() in a content-aware way — it inherits Object's identity-based versions, so two StringBuilders with identical text are not .equals() to each other. Always convert to String first (.toString()) if you need content-based comparison.
StringBuilder x = new StringBuilder("abc");
StringBuilder y = new StringBuilder("abc");
System.out.println(x.equals(y)); // false — identity comparison, inherited from Object
System.out.println(x.toString().equals(y.toString())); // true — compare as Strings insteadBest Practices
Default to StringBuilder unless you specifically need StringBuffer's thread-safety for a shared, mutable buffer accessed by multiple threads (and even then, consider whether a higher-level concurrency-safe design is more appropriate). Pre-size the buffer with an estimated capacity when building very large strings. Call .toString() exactly once at the end of building, rather than repeatedly mid-loop. Never rely on StringBuilder.equals() for content comparison.
Complete Example
public class BuilderDemo {
static String buildReport(String[] items) {
StringBuilder sb = new StringBuilder("Report:\n");
for (int i = 0; i < items.length; i++) {
sb.append(i + 1).append(". ").append(items[i]).append("\n");
}
return sb.toString();
}
public static void main(String[] args) {
String[] items = {"Apples", "Bread", "Cheese"};
System.out.println(buildReport(items));
// Report:
// 1. Apples
// 2. Bread
// 3. Cheese
StringBuilder palindromeCheck = new StringBuilder("level");
String reversed = palindromeCheck.reverse().toString();
System.out.println(reversed.equals("level")); // true
}
}Key Takeaways
StringBuilder and StringBuffer are mutable — append/insert/delete/replace/reverse edit the internal buffer in place instead of creating a new object each time.
StringBuilder is unsynchronized and faster; StringBuffer is synchronized (thread-safe per method call) and slower — StringBuilder is the default choice in modern single-threaded or externally-synchronized code.
Use StringBuilder instead of repeated String += concatenation in loops to avoid excessive intermediate object allocation.
Capacity is an internal buffer size hint that grows automatically as needed — it can be pre-sized for performance but never limits final size.
Neither class overrides equals()/hashCode() meaningfully — convert to String with toString() before comparing content.
String result = ""; for (int i = 0; i < n; i++) { result += data[i]; } // reallocates a new String each iteration
StringBuilder sb = new StringBuilder(); for (int i = 0; i < n; i++) { sb.append(data[i]); } String result = sb.toString();
Repeated += on a String creates a new object every iteration due to immutability; StringBuilder mutates one buffer in place across the whole loop, which is far more efficient for large n.
StringBuilder a = new StringBuilder("x"); StringBuilder b = new StringBuilder("x"); if (a.equals(b)) {... } // always false, comparing identity if (a.toString().equals(b.toString())) {... } // compares content correctly
StringBuilder does not override equals() for content comparison — it inherits Object's identity-based equals(), so two StringBuilders with identical text are never .equals() to each other unless converted to String first.
StringBuffer sb = new StringBuffer(); // used purely single-threaded, paying sync overhead for nothing
StringBuilder sb = new StringBuilder(); // same API, no unnecessary synchronization overhead
StringBuffer's synchronized methods only make sense when the buffer is genuinely shared and mutated by multiple threads; using it in single-threaded code pays synchronization overhead with no corresponding benefit — StringBuilder is the correct default.
Why is StringBuilder generally preferred over repeated String concatenation (+=) inside a loop?
Because String is immutable, each += typically allocates a brand-new String object combining the old content with the addition; over many loop iterations this creates significant allocation and copying overhead. StringBuilder maintains one mutable internal buffer that is edited in place across the whole loop, converting to a String only once at the end.
What is the single most significant functional difference between StringBuilder and StringBuffer?
StringBuffer's methods are synchronized, making it safe to use from multiple threads without external synchronization, at the cost of synchronization overhead on every call. StringBuilder has essentially the same API but is not synchronized, making it faster in single-threaded (or externally synchronized) code, which is the common case.
Does calling sb.append("x") on a StringBuilder create a new object, or does it modify sb in place?
It modifies sb in place, mutating its internal character buffer, and returns a reference to that same StringBuilder object (enabling method chaining like sb.append("a").append("b")) — no new StringBuilder object is created.
Why does sb1.equals(sb2) return false for two StringBuilder objects holding identical text?
StringBuilder does not override equals() (or hashCode()) for content comparison — it inherits Object's default identity-based equals(), which only returns true if both references point to the exact same object. To compare content, convert each to a String first with .toString() and compare those.
What does a StringBuilder's capacity represent, and what happens when you append more characters than the current capacity allows?
Capacity is the size of the internal character buffer, which can be larger than the current length to allow future appends without immediate reallocation. When an append would exceed the current capacity, the buffer typically grows automatically (a common growth strategy roughly doubles capacity, though the exact policy is a JVM/library implementation detail) — you never get an error from exceeding capacity.
What does delete(start, end) do on a StringBuilder, and is the end index inclusive or exclusive?
It removes the characters in the range from index start up to (but not including) index end — a half-open range, exactly like String.substring — mutating the StringBuilder in place and shifting the remaining characters left to fill the gap.
In modern code, when (if ever) is StringBuffer still the right choice over StringBuilder?
Only when the same mutable text buffer is genuinely shared and concurrently mutated by multiple threads without any other synchronization mechanism protecting it — StringBuffer's per-method synchronization then provides real safety. In the much more common single-threaded case, or where higher-level synchronization already exists, StringBuilder is the correct and faster default.
Write a method reverseWords that reverses the order of words in a sentence (not the characters within each word) using StringBuilder, e.g. "the sky is blue" -> "blue is sky the".
public class ReverseWordsDemo { static String reverseWords(String sentence) { String[] words = sentence.trim().split("\\s+"); StringBuilder sb = new StringBuilder(); for (int i = words.length - 1; i >= 0; i--) { sb.append(words[i]); if (i > 0) sb.append(' '); } return sb.toString(); } public static void main(String[] args) { System.out.println(reverseWords("the sky is blue")); // blue is sky the } } reverseWords splits the sentence into words on whitespace, then appends them into a StringBuilder from the last word to the first, inserting a single space between words (but not after the last one appended), producing the words in reversed order while each word's internal characters stay intact.
Trace through the chained calls and state the final printed value.
It prints "dlroW olleH <<". Step by step: append(" World") -> "Hello World"; insert(0, ">> ") -> ">> Hello World"; reverse() reverses the entire character sequence -> "dlroW olleH <<" (note the reversed characters of ">> " become " <<" at the end).
What does this print, and why do both variables see the appended text?
It prints "cats" then true. b = a copies only the reference, so a and b refer to the exact same StringBuilder object — appending through b mutates that shared object directly, so the change is visible through a too, and a == b is true because they are literally the same object, not just equal in content.
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World").insert(0, ">> ").reverse();
System.out.println(sb);StringBuilder a = new StringBuilder("cat");
StringBuilder b = a;
b.append("s");
System.out.println(a);
System.out.println(a == b);