Java StringBuilder
In Java, String objects are immutable — every concatenation or modification creates a brand-new object in memory rather than changing the existing one. When you need to build or edit text repeatedly, especially inside a loop, that immutability gets expensive fast. StringBuilder solves this by giving you a mutable, resizable sequence of characters that you can append to, insert into, delete from, and reverse in place, without allocating a new object on every single change.
It is one of the most commonly used utility classes in everyday Java code — used for building CSV rows, assembling SQL queries, formatting console output, and anywhere text is constructed piece by piece.
Overview / How It Works
A String in Java is backed by a character array (or, since Java 9, a compact byte array with a coder flag) that is final and can never change after the string is constructed. Every method that appears to “modify” a string — concat(), replace(), or the + operator — actually allocates a brand-new String and copies characters into it. Build a string with concatenation inside a loop that runs a thousand times, and you create roughly a thousand intermediate String objects, almost all of which are discarded immediately and become garbage for the collector to clean up.
StringBuilder, found in java.lang, takes a different approach. Internally it wraps a mutable char array (its capacity) along with a count of how many of those characters are actually in use (its length). When you call append(), insert(), or similar methods, StringBuilder writes directly into that backing array. If the array runs out of room, StringBuilder allocates a new, larger array — roughly double the old capacity — and copies the existing characters into it. This growth happens in big jumps rather than on every edit, so the number of array copies over the life of a StringBuilder is far smaller than the number of new String objects a chain of concatenations would create.
StringBuilder has a near-identical twin called StringBuffer. Both expose the same API; the only difference is that every method on StringBuffer is synchronized, making it safe to mutate from multiple threads at the cost of locking overhead on every call. StringBuilder is not synchronized. Unless you specifically need to share and mutate the same buffer across threads concurrently, StringBuilder is the correct default — it is also what the compiler itself uses internally when it optimizes a chain of + concatenations in a single expression.
Because a StringBuilder is mutable, methods like append(), insert(), delete(), and reverse() modify the object in place and return this. That is what makes method chaining possible, e.g. sb.append("a").append("b").reverse().
Syntax
StringBuilder has three constructors and a large family of mutating and query methods:
StringBuilder sb = new StringBuilder(); // empty, default capacity 16
StringBuilder sb2 = new StringBuilder(64); // empty, capacity 64
StringBuilder sb3 = new StringBuilder("Hello"); // pre-filled, capacity = 5 + 16
System.out.println(sb.length() + ", " + sb2.length() + ", " + sb3);
Output:
0, 0, Hello
| Constructor / Method | What it does |
|---|---|
StringBuilder() |
Empty builder, default capacity of 16 characters |
StringBuilder(int capacity) |
Empty builder with a chosen starting capacity — avoids resizing if you can estimate the final length |
StringBuilder(String str) |
Pre-loaded with str‘s characters; capacity = str.length() + 16 |
append(x) |
Adds the string form of x (any primitive, String, Object, etc.) to the end; returns this |
insert(int offset, x) |
Inserts x starting at index offset |
delete(int start, int end) |
Removes characters from start (inclusive) to end (exclusive) |
deleteCharAt(int index) |
Removes a single character at index |
replace(int start, int end, String str) |
Replaces the characters in [start, end) with str |
reverse() |
Reverses the character sequence in place |
setCharAt(int index, char c) |
Overwrites the character at index |
charAt(int index) |
Reads the character at index |
indexOf(String str) |
Returns the index of the first occurrence of str, or -1 |
length() / setLength(int n) |
Gets the current length, or truncates/pads it to n |
toString() |
Produces an immutable String snapshot of the current contents |
Examples
Example 1: Basic append, insert, and reverse
public class Main {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello");
sb.append(", World");
sb.append('!');
sb.insert(0, ">> ");
System.out.println(sb);
System.out.println("Length: " + sb.length());
sb.reverse();
System.out.println(sb);
}
}
Output:
>> Hello, World!
Length: 16
!dlroW ,olleH >>
Each call mutates the same object: append adds text at the end, insert(0, ...) pushes text to the front by shifting existing characters right, and reverse() flips the whole sequence in place. Notice that no new object is created between these calls — sb is the same StringBuilder throughout.
Example 2: Building a delimited string in a loop
public class Main {
public static void main(String[] args) {
StringBuilder csv = new StringBuilder();
for (int i = 1; i <= 10; i++) {
csv.append(i);
if (i < 10) {
csv.append(", ");
}
}
System.out.println("Numbers: " + csv);
System.out.println("Capacity check: " + csv.length() + " characters");
}
}
Output:
Numbers: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Capacity check: 29 characters
This is the classic use case: building a comma-separated list without knowing the final length ahead of time. The if (i < 10) check avoids a trailing separator by only adding ", " between elements, not after the last one.
Example 3: Masking, replacing, deleting, and inserting
public class Main {
public static void main(String[] args) {
StringBuilder account = new StringBuilder("1234567890123456");
for (int i = 0; i < account.length() - 4; i++) {
account.setCharAt(i, '*');
}
System.out.println("Masked: " + account);
StringBuilder message = new StringBuilder("The quick brown fox jumps over the lazy dog");
int index = message.indexOf("brown");
message.replace(index, index + "brown".length(), "red");
System.out.println(message);
message.delete(0, 4);
System.out.println(message);
message.insert(0, "A ");
System.out.println(message);
}
}
Output:
Masked: ************3456
The quick red fox jumps over the lazy dog
quick red fox jumps over the lazy dog
A quick red fox jumps over the lazy dog
This example combines several mutating methods on one buffer: setCharAt overwrites individual characters to mask all but the last four digits, indexOf locates a substring so replace can swap it out, delete trims characters from the front, and insert adds new text back at position 0. All four operations happen on the same underlying array without ever creating a new String until you choose to.
How It Works Step by Step (Under the Hood)
When you call new StringBuilder(), the JVM allocates a char[] of length 16 (the default capacity) and sets the internal count to 0. Every mutating call follows roughly this pattern:
- Capacity check — before writing, StringBuilder checks whether the backing array has enough room for the new characters.
- Grow if needed — if not, it computes a new capacity (typically
oldCapacity * 2 + 2, or exactly enough to fit the incoming data if that is larger), allocates a new array of that size, and copies the existing characters over withSystem.arraycopy. This copy is the only "expensive" step, and because capacity grows geometrically, it happens only a handful of times even after thousands of appends. - Write the characters — the new content is copied into the array starting at the appropriate offset, and
countis updated. - Return
this— so calls can be chained.
When you finally call toString(), StringBuilder copies the used portion of its internal array into a new, immutable String. Only at that point does an actual String object get created — everything before that was in-place mutation of one buffer.
Common Mistakes
Mistake 1: Concatenating strings with + inside a loop
String result = "";
for (int i = 0; i < 5; i++) {
result += i + ",";
}
System.out.println(result);
Output:
0,1,2,3,4,
This compiles and produces the correct output, but it is a performance trap: each += discards the old String and allocates an entirely new one, so a loop of n iterations does roughly O(n^2) character copying overall. The fix is to use a single StringBuilder for the whole loop:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
sb.append(i).append(",");
}
System.out.println(sb);
Output:
0,1,2,3,4,
Same output, but now the loop does at most a few array-growth copies instead of five separate String allocations.
Mistake 2: Reusing a StringBuilder without resetting it
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 3; i++) {
sb.append("Item ").append(i);
System.out.println(sb);
}
Output:
Item 1
Item 1Item 2
Item 1Item 2Item 3
This is a genuine bug, not just an inefficiency: because the same StringBuilder is reused across iterations without being cleared, each line contains everything appended so far instead of just the current item. The fix is to reset its length back to zero at the start of each iteration with setLength(0) (this keeps the backing array allocated, so it's cheaper than creating a new StringBuilder every time):
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 3; i++) {
sb.setLength(0);
sb.append("Item ").append(i);
System.out.println(sb);
}
Output:
Item 1
Item 2
Item 3
Best Practices
- Prefer StringBuilder over
+concatenation whenever you are building text inside a loop. - Pre-size the builder with an initial capacity (
new StringBuilder(200)) when you can estimate the final length, to reduce array-growth copies. - Use plain
StringBuilderfor single-threaded, local string building; reach forStringBufferonly when multiple threads genuinely share and mutate the same buffer concurrently. - Call
toString()once you are finished building, and pass the resulting immutableStringaround — avoid exposing a live, mutable StringBuilder to code that shouldn't be able to change it. - Chain calls for readability:
sb.append(a).append(b).append(c)instead of repeatingsb.append(...)on separate lines when the logic is simple. - Reuse a single StringBuilder with
setLength(0)in tight loops instead of allocating a new one on every iteration — but always remember to clear it first. - Don't over-optimize simple, one-off expressions like
"Hello, " + name + "!"— javac already compiles a single expression's concatenations into an efficient StringBuilder automatically. Manual StringBuilder use matters most across loop iterations or repeated calls.
Practice Exercises
- Exercise 1: Write a program that reverses each word in a sentence but keeps the word order unchanged (for example,
"Hello World"becomes"olleH dlroW"), using a StringBuilder for each word. - Exercise 2: Write a program that removes all vowels from a given string using StringBuilder's
deleteCharAtmethod. Hint: iterate over the indices backwards so removing a character doesn't shift the indices you still need to check. - Exercise 3: Write a method
String toCsv(int[] numbers)that builds a comma-separated string from an int array with no trailing comma, using only a StringBuilder (no String concatenation).
Summary
Stringis immutable; every apparent modification creates a new object.StringBuilderis mutable and edits its internal character array in place.- StringBuilder grows its backing array geometrically (roughly doubling) when it runs out of room, which keeps the amortized cost of repeated appends low.
- Key methods:
append,insert,delete,deleteCharAt,replace,reverse,setCharAt,indexOf, andsetLength. StringBufferis StringBuilder's synchronized, thread-safe twin — use it only when truly needed, since synchronization adds overhead.- Use StringBuilder instead of
+concatenation inside loops to avoid creating and discarding many intermediate String objects. - Always reset (
setLength(0)) a reused StringBuilder before writing new content into it, or leftover characters will silently corrupt your output. - Call
toString()to get an immutable snapshot once the building is finished.
