The report that took forever
Riya writes a program to print the fest's registration report: one line for each of 50,000 students, built with report += line;. For 50 students it was instant. For 50,000 it runs, and runs, and the laptop fan starts screaming.
Sir, it's just adding one line at a time. Why is it so slow?
Because Strings never change. Every += builds a brand-new String and copies the whole report so far into it. Line 50,000 copies 49,999 lines. That's crores of characters copied.
So I need a String that can change?
Exactly: StringBuilder. It keeps one buffer with spare room and just writes the new line at the end. Same code shape, and your report prints in a blink.
What are StringBuilder and StringBuffer?
StringBuilder and StringBuffer are classes in java.lang that represent a mutable sequence of characters. They store characters in an internal buffer with a capacity that grows automatically. Methods like append, insert, delete and reverse modify the same object. StringBuffer (Java 1.0) is synchronized and thread-safe; StringBuilder (Java 5) is not synchronized and is faster for single-threaded use.
A String is a printed card. A StringBuilder is a whiteboard: write more, erase a bit, insert a word, and when you're done, take a photo with toString().
Words you'll meet in this lesson
append returns the same builder, so calls can be joined.String vs StringBuilder vs StringBuffer
| String | StringBuilder | StringBuffer | |
|---|---|---|---|
| Mutable? | No | Yes | Yes |
| Thread-safe? | Yes (immutable) | No | Yes (synchronized) |
| Speed for building text | Slow in loops | Fastest | Slightly slower (locking) |
equals() compares text? | Yes | No (same object only) | No (same object only) |
| Since | Java 1.0 | Java 5 | Java 1.0 |
| Use for | Values, keys, constants | Building text in a method | Legacy code; rarely needed today |
Exam tip: new StringBuilder() has capacity 16; new StringBuilder("abc") has capacity 16 + 3 = 19. When full, the new capacity is old × 2 + 2 (16 → 34), or more if needed.
The editing methods
public class Main {
public static void main(String[] args) {
StringBuilder s = new StringBuilder("Hello World");
s.insert(5, ","); System.out.println(s);
s.delete(5, 6); System.out.println(s);
s.replace(6, 11, "Java"); System.out.println(s);
s.setCharAt(0, 'J'); System.out.println(s);
s.deleteCharAt(0); System.out.println(s);
s.reverse(); System.out.println(s);
}
}| Method | What it does |
|---|---|
append(x) | Adds x (any type) at the end; returns the same builder |
insert(i, x) | Puts x before index i, shifting the rest right |
delete(start, end) | Removes indexes start to end − 1 (end excluded, like substring) |
deleteCharAt(i) | Removes one character |
replace(start, end, str) | Replaces a range with str (note: a range, unlike String.replace) |
setCharAt(i, ch) | Overwrites one character: impossible with String |
reverse() | Reverses in place |
length(), charAt, indexOf, substring | Read-only methods, like String's |
toString() | Copies the current contents into a new, immutable String |
The equals() trap: StringBuilder doesn't override equals, so new StringBuilder("hi").equals(new StringBuilder("hi")) is false. Compare a.toString().equals(b.toString()), or a.compareTo(b) == 0 (Java 11+). Don't use builders as HashMap keys.
Building text the fast way
Create one builder
One object with an empty buffer of 16 characters. If you know the result will be big, pass a size:StringBuilder sb = new StringBuilder();new StringBuilder(10_000).Append in the loop
Each append writes characters into the spare room at the end. No new objects, and earlier text is never copied, except when the buffer has to grow.for (int i = 1; i <= 5; i++) { sb.append("Row ").append(i).append('\n'); }Edit anywhere
Insert shifts the existing characters right inside the buffer. Handy, but inserting at the front of a huge builder many times is slow: append when you can.sb.insert(0, "REPORT\n");Grow when full
If the text no longer fits, the builder makes a bigger array (about double) and copies once.
Doubling means growth happens rarely: building n characters costs about n copies in total, instead of about n² with+=.Finish with toString()
Creates an immutable String snapshot. Later changes toString report = sb.toString();sbdon't affectreport. Pass Strings around, keep builders local.
One buffer, edited in place: both views
Left: the code and the builder's contents. Right: the builder's internal buffer, its length and its capacity.
StringBuilder 6 steps- Create: an empty builder. Printing it shows nothing.
- append("Java"): the builder now holds Java. No new object was made.
- Chain:
appendreturns the same builder, so.append(' ').append(21)keeps adding. Numbers become text. - insert(0, "Hi "): text is placed before index 0.
- Grow: 17 characters don't fit in 16, so the builder quietly gets a bigger buffer.
- toString(): copies the contents into a normal String you can store, compare and pass around.
- Inside the builder is an array with room for 16 characters and a counter: length 0.
appendwrites into the next free slots and increases the length. Capacity is unchanged.- The same object (#1) keeps being updated. Compare with String, where each step made a new object.
insertmoves existing characters to the right inside the array, then writes the new ones in front.- 17 > 16: the builder allocates a bigger array (16 × 2 + 2 = 34), copies the old characters once, and carries on. Growing by doubling makes this rare.
toString()creates an independent immutable String. That's what you return or store.
StringBuilder in real systems
Any time text is assembled piece by piece, especially in loops, a builder is used directly or behind the scenes.
Watch it with an example: building a receipt line by line
A shop app adds 3 items to a receipt. Compare String += (a new object every time) with one StringBuilder that grows in place.
- String:
r += "Tea 10;"creates String #1 and pointsrto it. - Second item: Strings can't change, so Java builds a brand-new #2 with the old text copied plus the new item. #1 becomes garbage.
- Third item: again a full copy into #3. With n items you create n objects and copy the text again and again.
- StringBuilder: one object with an internal char buffer (default capacity 16).
appendwrites into the free space. - Second append: 15 characters, still inside the 16-slot buffer. No new object, no copy.
- Growing, then finish: the third item needs 23 slots, so the buffer is replaced once by a bigger one (34 slots; it roughly doubles).
toString()then makes the final String once. Use StringBuilder for loops that build text: reports, CSV exports, SMS and email bodies. (StringBuffer is the older, synchronized version.)
1. Reports and exports
CSV downloads, statements and audit reports are built line by line. With a builder, a 1 lakh-row export takes milliseconds instead of minutes.
2. Dynamic queries and URLs
Search filters add conditions only when the user picks them: WHERE, then AND city = ?, then AND price < ?. Values still go in as parameters, never pasted into the text.
3. Messages, HTML and JSON
Email bodies, simple HTML pages and JSON payloads are often assembled with a builder or by libraries that use one internally.
4. What javac does for you
A single expression like "Hi " + name + "!" is already compiled efficiently (Java 9+ uses a special concatenation call). The problem is only += repeated across loop iterations.
Examples are simplified. Very large outputs are usually written straight to a file or network stream rather than built fully in memory.
Riya's report, fixed
public class Main {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 5; i++) {
sb.append("Row ").append(i).append('\n');
}
sb.insert(0, "REPORT\n");
sb.append("END");
System.out.println(sb);
System.out.println("Characters: " + sb.length());
String report = sb.toString();
System.out.println(report.startsWith("REPORT"));
}
}sb.append("Row ").append(i).append('\n')Chaining works because each append returns the same builder.sb.insert(0, "REPORT\n")Adds the heading in front after the rows are built.System.out.println(sb)println callstoString()for you.String report = sb.toString()Turn it into a String before returning, comparing or storing it.
Building "1-2-3" in one object
Watch the heap: the StringBuilder (#1) changes in place. Only toString() creates a String.
Worked examples: real problems, solved and traced in memory
Three real jobs where text is built or edited step by step. Run each solution, then step through it and watch a single StringBuilder object change in place, including its length and capacity.
Build a report and watch the buffer grow
Problem statement
An operations report lists the rows processed today, one per line. Build the text with a StringBuilder and print it, along with the final length and capacity.
rows = 3
Row 1 Row 2 Row 3 18 chars, capacity 34
Approach
- Create one StringBuilder (capacity 16 by default).
- Append each line in the loop; no new String objects are created per step.
- When the text no longer fits, the builder grows its buffer once (16 × 2 + 2 = 34).
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Mask a card number in place
Problem statement
A receipt must show only the last 4 digits of a card number. Write mask(card) that replaces every earlier digit with X using a StringBuilder, and returns the masked String.
card = "41112222" (shortened to 8 digits to keep the trace short)
XXXX2222
Approach
- Copy the card number into a StringBuilder: Strings can't be edited, builders can.
- Loop over all positions before the last 4 and call
setCharAt(i, 'X'). - Return
toString(), an immutable snapshot for the receipt.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
A method that adds to the caller's cart summary
Problem statement
A cart summary is built by a helper addItem(summary, item, price) that appends one line each time it's called. Unlike a String, the caller's builder must show every added item afterwards.
items: ("Dosa", 90), ("Coffee", 40)CART Dosa Rs 90 Coffee Rs 40
Approach
- Create the summary builder once in main.
- Pass it to
addItem: the method receives a copy of the reference, so both point to the same object. appendchanges that shared object, so main sees the result without any return value.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Check yourself
Quick revision
Lesson 4.6 in one screen
- StringBuilder / StringBuffer = mutable text; methods change the same object.
append,insert,delete(start, end),deleteCharAt,replace(start, end, s),setCharAt,reverse.- Methods return the same builder → chaining.
- Capacity 16 by default (16 + text length with a String); grows to old × 2 + 2.
- Use a builder for text built in loops; one-line
+is fine. equalson builders compares identity: usetoString().equals.- StringBuffer = synchronized; prefer StringBuilder as a local variable.
Interview and viva questions
String vs StringBuilder vs StringBuffer?
String is immutable and thread-safe because it can't change. StringBuilder is mutable and fastest, but not thread-safe. StringBuffer is mutable and synchronized (thread-safe), with some locking overhead. Use String for values, StringBuilder for building text, and StringBuffer only when legacy APIs need it.
Is "a" + b + "c" slow? Should every + become a StringBuilder?
No. A single concatenation expression is compiled efficiently (with StringBuilder in old Java, with invokedynamic-based concatenation since Java 9). Only repeated += across loop iterations causes the n² copying problem.
How does StringBuilder grow?
It keeps a character array with spare capacity. When an append doesn't fit, it allocates a new array of about twice the size (old × 2 + 2, or the required size if larger) and copies the old characters once.
Why doesn't StringBuilder override equals and hashCode?
Because it's mutable: its content can change at any time, so content-based equality and hash codes would be unreliable (for example, as HashMap keys). Convert to String to compare.
Is StringBuffer enough to make code thread-safe?
Each single method call is synchronized, but a sequence of calls (check length, then append) isn't atomic. In practice, give each thread its own StringBuilder as a local variable; that's both safe and fast.
Practice problems
Whenever you build or edit text step by step, reach for StringBuilder. Try each problem with String first in your head, then notice how much simpler the builder makes it. The last three are real business tasks.
Reverse a sentence's characters
Reverse a string using StringBuilder.
s = "MyPhatshala"
alahstahPyM
Show solution and explanation
public class Main {
public static void main(String[] args) {
String s = "MyPhatshala";
String rev = new StringBuilder(s).reverse().toString();
System.out.println(rev);
}
}How it works: wrap the String in a builder, call reverse() (which changes the builder in place and returns it), then toString() to get a String back. One object does the work, instead of one new String per character.
Comma-separated list without a trailing comma
Print the numbers 1 to 10 as 1, 2, 3, …, 10, with no comma after the last one.
n = 10
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Show solution and explanation
public class Main {
public static void main(String[] args) {
int n = 10;
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= n; i++) {
if (sb.length() > 0) sb.append(", ");
sb.append(i);
}
System.out.println(sb);
}
}How it works: add the separator before every item except the first (when the builder is still empty). append accepts ints, chars, booleans and more. println(sb) calls toString() automatically.
Sentence palindrome
Check if a sentence is a palindrome when you ignore spaces, punctuation and case.
s = "Was it a car or a cat I saw?"
wasitacaroracatisaw -> palindrome
Show solution and explanation
public class Main {
public static void main(String[] args) {
String s = "Was it a car or a cat I saw?";
StringBuilder clean = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if (Character.isLetterOrDigit(ch)) clean.append(Character.toLowerCase(ch));
}
String forward = clean.toString();
String backward = clean.reverse().toString();
System.out.println(forward + " -> " + (forward.equals(backward) ? "palindrome" : "not a palindrome"));
}
}How it works: keep only letters and digits, lower-cased. Take the String before reversing, because reverse() changes the builder itself. Compare with equals; builders themselves don't compare content with equals.
Remove all vowels
Delete every vowel from a word, editing a builder in place.
word = "Education"
dctn
Show solution and explanation
public class Main {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Education");
for (int i = sb.length() - 1; i >= 0; i--) {
if ("aeiouAEIOU".indexOf(sb.charAt(i)) != -1) {
sb.deleteCharAt(i);
}
}
System.out.println(sb);
}
}How it works: deleting shifts the later characters left. Going backwards means the shift only affects indexes we've already checked. A forward loop would skip the character right after each deletion. "aeiouAEIOU".indexOf(ch) is a quick “is it a vowel?” test.
Run-length compression
Compress repeated letters: aaabbcdddd becomes a3b2c1d4.
s = "aaabbcdddd"
a3b2c1d4
Show solution and explanation
public class Main {
public static void main(String[] args) {
String s = "aaabbcdddd";
StringBuilder out = new StringBuilder();
int count = 1;
for (int i = 1; i <= s.length(); i++) {
if (i < s.length() && s.charAt(i) == s.charAt(i - 1)) {
count++;
} else {
out.append(s.charAt(i - 1)).append(count);
count = 1;
}
}
System.out.println(out);
}
}How it works: count while the character repeats; when it changes (or the string ends, i == length), append the letter and its count. Running i one past the end is what flushes the last group. Chained append calls keep it compact.
Remove duplicate characters
Keep only the first occurrence of each character.
s = "programming"
progamin
Show solution and explanation
public class Main {
public static void main(String[] args) {
String s = "programming";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if (sb.indexOf(String.valueOf(ch)) == -1) {
sb.append(ch);
}
}
System.out.println(sb);
}
}How it works: before adding a character, check whether the builder already contains it. StringBuilder's indexOf takes a String, so the char is converted with String.valueOf.
Capitalise with setCharAt
Make the first letter of each word upper case by editing the builder directly.
s = "learn java visually"
Learn Java Visually
Show solution and explanation
public class Main {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("learn java visually");
for (int i = 0; i < sb.length(); i++) {
if (i == 0 || sb.charAt(i - 1) == ' ') {
sb.setCharAt(i, Character.toUpperCase(sb.charAt(i)));
}
}
System.out.println(sb);
}
}How it works: unlike String, a builder can change one character in place with setCharAt. A letter starts a word if it's at index 0 or follows a space. No new objects are created inside the loop.
Insert a separator every 4 characters
Format a 16-digit card-style number as groups of 4: 1234 5678 9012 3456.
num = "1234567890123456"
1234 5678 9012 3456
Show solution and explanation
public class Main {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("1234567890123456");
for (int i = sb.length() - 4; i > 0; i -= 4) {
sb.insert(i, ' ');
}
System.out.println(sb);
}
}How it works: inserting from the right means earlier positions aren't shifted by the spaces we've already added. Positions 12, 8 and 4 get a space.
Indian-style amount formatting
Format amounts with Indian digit grouping: the last 3 digits, then groups of 2. 1234567 → 12,34,567.
amounts = 999, 15000, 1234567, 250000000
Rs 999 Rs 15,000 Rs 12,34,567 Rs 25,00,00,000
Show solution and explanation
public class Main {
public static void main(String[] args) {
long[] amounts = {999, 15000, 1234567, 250000000};
for (long amt : amounts) {
StringBuilder sb = new StringBuilder(String.valueOf(amt));
int pos = sb.length() - 3;
while (pos > 0) {
sb.insert(pos, ',');
pos -= 2;
}
System.out.println("Rs " + sb);
}
}
}How it works: the first comma goes 3 digits from the right, then every 2 digits (lakh, crore). Working right to left with insert keeps positions valid. Invoices and bank apps in India show amounts this way; Java's NumberFormat with an Indian locale can also do it.
Build a CSV report
Turn arrays of names and marks into CSV text (a header line plus one line per student) ready to save as a file.
names = {"Riya", "Kabir", "Meera"}, marks = {{78, 92}, {81, 67}, {95, 88}}name,maths,science,total Riya,78,92,170 Kabir,81,67,148 Meera,95,88,183
Show solution and explanation
public class Main {
public static void main(String[] args) {
String[] names = {"Riya", "Kabir", "Meera"};
int[][] marks = {{78, 92}, {81, 67}, {95, 88}};
StringBuilder csv = new StringBuilder("name,maths,science,total\n");
for (int i = 0; i < names.length; i++) {
int total = marks[i][0] + marks[i][1];
csv.append(names[i]).append(',')
.append(marks[i][0]).append(',')
.append(marks[i][1]).append(',')
.append(total).append('\n');
}
System.out.print(csv);
}
}How it works: one builder collects the whole file. Chained append calls add each field and separator without creating intermediate Strings. For thousands of rows, this is dramatically faster than +=.
Mask a card number
On a receipt, show only the last 4 digits of a card number, keeping the spaces: XXXX XXXX XXXX 3456.
card = "1234 5678 9012 3456"
XXXX XXXX XXXX 3456
Show solution and explanation
public class Main {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("1234 5678 9012 3456");
int digitsSeen = 0;
for (int i = sb.length() - 1; i >= 0; i--) {
if (Character.isDigit(sb.charAt(i))) {
digitsSeen++;
if (digitsSeen > 4) sb.setCharAt(i, 'X');
}
}
System.out.println(sb);
}
}How it works: walk from the right, counting digits; after the 4th digit, overwrite each digit with X using setCharAt. Spaces are left alone. Card rules (PCI DSS) require full numbers never to be displayed or printed.
Real-world scenario questions (asked in company interviews)
Builders fix performance problems, but shared or misused builders cause new bugs. Decide your answer first, then open the model answer.
“Replace every + with StringBuilder”
A reviewer asks you to change String msg = "Order " + id + " placed by " + name; to use StringBuilder “for performance”, while a loop elsewhere uses html += row; for 10,000 rows. What do you say?
The interviewer is checking: do you know where the cost really is, instead of following rules blindly?
Show a strong answer
- The one-line message is already compiled efficiently; rewriting it only hurts readability.
- The loop is the real problem: each
+=copies the growing HTML, about n² work. Change that to a StringBuilder (with an estimated initial capacity). - Back the change with a quick measurement so the review is about evidence.
Avoid: applying StringBuilder everywhere “just in case”.
Mixed-up responses under load
To “save memory”, a developer made a static StringBuilder sb shared by a request handler, calling sb.setLength(0) at the start of each request. Under load, users occasionally receive parts of other users' responses, and sometimes an exception. Explain.
The interviewer is checking: do you understand that StringBuilder is not thread-safe and shared state is dangerous?
Show a strong answer
- A web server handles requests on many threads at once. All of them clear and append to the same buffer, so text from different users mixes, and concurrent resizing can even throw exceptions.
- Fix: make the builder a local variable inside the method. Each request gets its own, which is cheap and needs no locking.
- Switching to StringBuffer would stop crashes but still mix content, because a whole response is many calls, not one.
- This is also a data-leak risk: one user's personal data could appear in another's response.
Avoid: “Use StringBuffer and it's fixed.” The design (shared state) is the bug.
The duplicate check that never finds duplicates
Code builds a product key for each cart item with a StringBuilder (brand + size + colour) and adds it to a HashSet<StringBuilder> to detect duplicate items. Duplicates are never detected. Why?
The interviewer is checking: do you know StringBuilder doesn't implement content equality?
Show a strong answer
- HashSet uses
equalsandhashCode. StringBuilder inherits them from Object, so two builders with the same text are different keys. - Convert to a String:
set.add(sb.toString())with aHashSet<String>. - Even if it worked, a mutable key that changes after insertion would get lost in the set. Keys should be immutable.
Avoid: keeping builders around after construction. Build, then toString().
Out of memory on the year-end statement
A service builds a corporate customer's full-year statement (about 200 MB of text) in a StringBuilder, then writes it to a file. It fails with OutOfMemoryError for the biggest customers. The team asks whether a larger initial capacity would help. What do you recommend?
The interviewer is checking: do you know builder growth costs and when to stream instead?
Show a strong answer
- Each growth step briefly needs both the old and the new array, and Java chars can take 2 bytes each, so peak memory can be several times the final text size.
- A good initial capacity avoids repeated copies and reduces the peak, which helps for moderately large outputs.
- For hundreds of MB, don't hold it all in memory: write each line straight to a
BufferedWriteror the HTTP response stream as you produce it. Memory then stays small and constant. - Consider generating big statements asynchronously and compressing them.
Avoid: just increasing the JVM heap. The next, bigger customer will fail again.