Java StringBuilder and StringBuffer: Mutable Strings, Methods and Performance
These are the notes for Java episode 10. You will learn why s = s + i in a loop is slow (we time it live with System.currentTimeMillis()), what a StringBuilder is (text you can change in place), its methods append, insert, replace, delete, deleteCharAt, setCharAt and reverse, method chaining, a one-line palindrome check, three StringBuilder traps, capacity and how a builder grows, and StringBuffer, the synchronized twin for many threads. Every example below runs as-is in the SyntaxPilot compiler, with its real output from javac 21 (timings vary from machine to machine and run to run).
The problem: every ticket number in one line
The college fest sold 50,000 tickets, numbered from 0. The gate scanner wants them all in one line: 0,1,2,3,.... The obvious way, from episode 9, is s = s + i + "," in a loop. This time we also time it.
System.currentTimeMillis() returns the current time in milliseconds (thousandths of a second) as a long. Take it before and after, and subtract.
public class Main {
public static void main(String[] args) {
long start = System.currentTimeMillis();
String s = "";
for (int i = 0; i < 50000; i++) {
s = s + i + ",";
}
long end = System.currentTimeMillis();
System.out.println("String: " + (end - start) + " ms, " + s.length() + " chars");
}
}
String: 1405 ms, 288890 chars
Well over a second for one String. With 100,000 tickets it took about 5,200 ms on the same machine: double the work, roughly four times the time.
Why: Strings are immutable. Every
+builds a new String and copies every old character into it. Round 1 copies 2 characters, round 2 copies 4, ..., round 50,000 copies about 289,000. Added up, the loop copies billions of characters just to throw them away. The cost grows with the square of the number of rounds.
The fix: StringBuilder
Real-life example: a whiteboard.
| In a classroom | In Java |
|---|---|
| a printed certificate: any change means a reprint | String: immutable |
| a whiteboard: write, erase, add more on the same board | StringBuilder: mutable, changed in place |
| take a photo when you're done | sb.toString(): a fixed String to keep |
Mutable means "can be changed after it's created".
public class Main {
public static void main(String[] args) {
long start = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 50000; i++) {
sb.append(i).append(",");
}
String s = sb.toString();
long end = System.currentTimeMillis();
System.out.println("StringBuilder: " + (end - start) + " ms, " + s.length() + " chars");
}
}
StringBuilder: 11 ms, 288890 chars
The same 288,890 characters, more than a hundred times faster. append writes at the end of the existing text without copying it.
Method chaining: append returns the same builder, so you can call the next method straight on it: sb.append(i).append(",").
Note: StringBuilder lives in java.lang, like String, so it needs no import.
StringBuilder methods
| Method | What it does |
|---|---|
append(x) |
adds x (any type) at the end |
insert(index, x) |
inserts x at index |
replace(start, end, str) |
replaces characters start to end - 1 with str |
delete(start, end) |
removes characters start to end - 1 |
deleteCharAt(i) |
removes the character at i |
setCharAt(i, c) |
changes the character at i to c |
reverse() |
reverses the text |
charAt(i), indexOf(str), length() |
read it, like a String |
toString() |
returns the text as a String |
Like substring, ranges are start included, end excluded. Every editing method changes the builder itself and returns it (so they chain).
public class Main {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("chai");
sb.append(" 20");
System.out.println(sb);
sb.insert(0, "Masala ");
System.out.println(sb);
sb.replace(12, 14, "25");
System.out.println(sb);
sb.delete(0, 7);
System.out.println(sb);
sb.setCharAt(0, 'C');
System.out.println(sb + " | length " + sb.length());
}
}
chai 20
Masala chai 20
Masala chai 25
chai 25
Chai 25 | length 7
M a s a l a _ c h a i _ 2 0
0 1 2 3 4 5 6 7 8 9 ... 12 13 replace(12, 14, "25") swaps index 12 and 13
println(sb) works directly because println calls toString() for us; so does sb + " | ...".
Palindrome check in one line
public class Main {
public static void main(String[] args) {
String[] words = {"madam", "level", "java"};
for (String w : words) {
String back = new StringBuilder(w).reverse().toString();
System.out.println(w + " -> " + back + " : " + w.equals(back));
}
}
}
madam -> madam : true
level -> level : true
java -> avaj : false
new StringBuilder(w) makes a builder holding a copy of w; reversing it never changes w.
Three StringBuilder traps
1. A StringBuilder is not a String
StringBuilder sb = new StringBuilder("Priya");
String name = sb;
Main.java:4: error: incompatible types: StringBuilder cannot be converted to String
String name = sb;
^
1 error
Fix: String name = sb.toString();
2. equals on two builders compares objects, not text
public class Main {
public static void main(String[] args) {
StringBuilder a = new StringBuilder("chai");
StringBuilder b = new StringBuilder("chai");
System.out.println(a.equals(b));
System.out.println(a.toString().equals(b.toString()));
System.out.println(a.compareTo(b) == 0);
}
}
false
true
true
String overrides equals to compare characters; StringBuilder doesn't, so its equals only asks "same object?". Compare toString()s, or use compareTo (Java 11+), which returns 0 when the text is the same.
3. Two names, one builder
public class Main {
public static void main(String[] args) {
StringBuilder order = new StringBuilder("tea");
StringBuilder copy = order;
copy.append(" + bun");
System.out.println(order);
}
}
tea + bun
copy = order copies the reference, as with arrays in episode 7. Both names point to one builder. With Strings this never hurts, because nobody can change a String; with a mutable object, it does. A real copy: new StringBuilder(order).
Capacity: how a StringBuilder grows
Real-life example: a notebook with spare pages.
| A notebook | In Java |
|---|---|
| pages already written | length(): characters stored |
| blank pages left | capacity(): room before it must grow (16 for an empty builder) |
| full? move to a thicker notebook, copying once | grows automatically, to about double + 2 |
public class Main {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
System.out.println(sb.length() + " / " + sb.capacity());
sb.append("Registration desk");
System.out.println(sb.length() + " / " + sb.capacity());
StringBuilder big = new StringBuilder(1000);
System.out.println(big.length() + " / " + big.capacity());
}
}
0 / 16
17 / 34
0 / 1000
17 characters didn't fit in 16, so the builder grew to 16 × 2 + 2 = 34. Growing copies the text, but only now and then (each time the room doubles), not on every append like String +. If you know roughly how big the text will be, ask for that capacity up front: new StringBuilder(1000).
StringBuffer: the synchronized twin
A thread is one job a program runs at the same time as other jobs (like two gate scanners writing to one log). We build threads later in the course.
- If two threads append to one
StringBuilderat the same moment, characters can get mixed up or lost: StringBuilder is not thread safe. StringBufferhas the same methods, but every method is synchronized: it takes a lock, so only one thread can use the buffer at a time. That makes it thread safe.- The lock costs a little speed. Use
StringBuilderfor one thread (almost always),StringBufferwhen many threads share one text.
public class Main {
public static void main(String[] args) {
StringBuffer log = new StringBuffer();
log.append("Gate 1 open").append(" | ").append("Gate 2 open");
log.reverse().reverse();
System.out.println(log + " | length " + log.length());
}
}
Gate 1 open | Gate 2 open | length 25
String |
StringBuilder |
StringBuffer |
|
|---|---|---|---|
| can change? | no (immutable) | yes (mutable) | yes (mutable) |
| thread safe? | yes (it can't change) | no | yes (synchronized) |
| speed for many edits | slow | fastest | fast, a little slower |
| since | Java 1.0 | Java 5 | Java 1.0 |
| use for | fixed text, a few joins | building text, loops | text shared by many threads |
Note: for a single line like "Hi " + name + "!", the compiler already does the joining efficiently. StringBuilder matters when you add in a loop or edit text many times.
Common mistakes (and the real error messages)
| Mistake | Real message | Why | Fix |
|---|---|---|---|
s = s + i; in a big loop |
no error, just very slow | every + copies the whole String |
sb.append(i); |
String name = sb; |
incompatible types: StringBuilder cannot be converted to String |
different types | sb.toString() |
a.equals(b) on two builders |
no error, false |
compares objects | a.toString().equals(b.toString()) or a.compareTo(b) == 0 |
sb.charAt(10) when length is 4 |
StringIndexOutOfBoundsException: Index 10 out of bounds for length 4 |
indexes go 0 to length() - 1 |
check sb.length() |
StringBuilder copy = order; to copy |
no error, both change together | copies the reference | new StringBuilder(order) |
sb.reverse() expecting the original kept |
no error, the builder is reversed | builders change themselves | reverse a copy |
Mini project: chai stall bill
public class Main {
public static void main(String[] args) {
String[] items = {"Masala chai", "Samosa", "Bun maska"};
int[] qty = {2, 3, 1};
int[] price = {20, 15, 30};
StringBuilder bill = new StringBuilder("--- Raju Chai Stall ---\n");
int total = 0;
for (int i = 0; i < items.length; i++) {
int cost = qty[i] * price[i];
total += cost;
bill.append(items[i]).append(" x").append(qty[i]).append(" = Rs ").append(cost).append("\n");
}
bill.append("Total: Rs ").append(total);
System.out.println(bill);
}
}
--- Raju Chai Stall ---
Masala chai x2 = Rs 40
Samosa x3 = Rs 45
Bun maska x1 = Rs 30
Total: Rs 115
\n is the new line escape: it ends the line inside the text. Extend it: add 5% GST on the total, and a "Thank you, visit again!" line.
Practice
- Build
1-2-3-...-20with no dash at the end. - Remove all spaces from a sentence using
deleteCharAt. - Hide an email with
setCharAt:priya@gmail.com→p****@gmail.com. - Time String
+vs StringBuilder for 20,000 rounds on your machine. - Reverse each word of a sentence but keep the word order:
chai is hot→iahc si toh.
Solutions and hints
java StringBuilder sb = new StringBuilder(); for (int i = 1; i <= 20; i++) { sb.append(i); if (i < 20) { sb.append("-"); } } System.out.println(sb);(Or append"-"every time andsb.deleteCharAt(sb.length() - 1)at the end.)- Walk backwards, so deleting doesn't shift the characters you haven't checked yet:
java StringBuilder sb = new StringBuilder("chai is hot"); for (int i = sb.length() - 1; i >= 0; i--) { if (sb.charAt(i) == ' ') { sb.deleteCharAt(i); } } System.out.println(sb); // chaiishot java StringBuilder e = new StringBuilder("priya@gmail.com"); for (int i = 1; i < e.indexOf("@"); i++) { e.setCharAt(i, '*'); } System.out.println(e); // p****@gmail.com- Copy the two timed programs above and change
50000to20000. java StringBuilder out = new StringBuilder(); for (String w : "chai is hot".split(" ")) { out.append(new StringBuilder(w).reverse()).append(" "); } System.out.println(out.toString().strip()); // iahc si toh
Quick quiz
What does this print?
StringBuilder sb = new StringBuilder("abc");
sb.append("d").reverse();
System.out.println(sb + " " + sb.length());
- A.
abcd 4 - B.
dcba 4 - C.
dcba 3 - D.
abc 3
Answer
B. dcba 4. A StringBuilder changes itself: append("d") makes it abcd, and reverse() flips the same builder to dcba. Nothing needs to be stored. Compare with episode 9's quiz, where s.toUpperCase() changed nothing.
Interview questions
- String vs StringBuilder vs StringBuffer? String is immutable; StringBuilder is mutable and not synchronized (fastest); StringBuffer is mutable and synchronized (thread safe).
- Why is String concatenation in a loop slow? Each
+creates a new String and copies all previous characters: O(n²) total work. - What is the default capacity of a StringBuilder, and how does it grow? 16; when full it grows to about
(old × 2) + 2. - Does
StringBuilder.equalscompare content? No; it's inherited fromObjectand compares references. - Which was added later, StringBuilder or StringBuffer, and why? StringBuilder, in Java 5, as a faster unsynchronized version for the common single-thread case.
- Is
"a" + b + "c"on one line slow? No; the compiler handles single expressions efficiently. The problem is repeated+=in a loop. - How do you convert a StringBuilder to a String?
sb.toString()(orString.valueOf(sb)).
Glossary
| Term | Meaning | Example |
|---|---|---|
| mutable | can be changed after it's created | a StringBuilder |
| immutable | can never change after it's created | a String |
| StringBuilder | Java's mutable text class, not synchronized | new StringBuilder() |
| StringBuffer | the same as StringBuilder, but synchronized | new StringBuffer() |
append |
add to the end | sb.append(5) |
| method chaining | calling the next method on the returned object | sb.append(a).append(b) |
toString() |
turns a builder (or any object) into a String | sb.toString() |
| capacity | how many characters a builder can hold before it grows | 16 at first |
| millisecond | one thousandth of a second | 1405 ms ≈ 1.4 s |
System.currentTimeMillis() |
the current time in milliseconds, as a long |
stopwatch start/end |
| thread | a job the program runs at the same time as others | two gate scanners |
| thread safe | works correctly when many threads use it at once | StringBuffer |
| synchronized | locked so only one thread runs it at a time | StringBuffer's methods |
| reference | a variable's link to an object | copy = order |
\n |
the new line escape inside text | "Total\n" |
| palindrome | a word that reads the same backwards | madam |
Summary
s = s + xin a loop copies everything every time: slow (it grows with the square of the rounds).- StringBuilder is mutable text:
append,insert,replace,delete,deleteCharAt,setCharAt,reversechange it in place, and they chain. toString()turns it into a String; a builder'sequalscompares objects, not text.- Capacity starts at 16 and grows to about double; ask for more up front if you know the size.
- StringBuffer: same methods, synchronized, for text shared by many threads.
Next episode: Methods: write the steps once, give them a name, use them everywhere. Plus pass-by-value and overloading.