StringBuilder & String Formatting
Reviewed & published by Brayan K
Strings in Java are immutable, which makes glueing lots of text together with + surprisingly wasteful. Meet StringBuilder for efficient text assembly, and printf/String.format for clean, aligned, professional output.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Why Java Strings are immutable
- The hidden cost of + in a loop
- Building text with StringBuilder.append
- insert, length, and toString
- printf and String.format specifiers
- Width, alignment, padding, and %n / %%
1️⃣ Strings Are Immutable
A String can never be changed after it's created. When you write s = s + "!", Java doesn't edit the old text — it builds a brand-new String and points s at it. The original is left untouched (and eventually garbage-collected).
💡 Analogy: A String is like a printed page — you can't erase a word on it. To "change" it you photocopy the page, write the new version, and throw the old copy away. Do that a million times in a loop and you've made a million photocopies.
This is usually fine. It only becomes a performance problem when you concatenate inside a large loop, where the cost grows quadratically.
public class Main {
public static void main(String[] args) {
// Strings are IMMUTABLE: every "change" makes a brand-new object.
String s = "Hello";
s = s + " World"; // does NOT modify "Hello" — builds a new String
System.out.println(s);
// The hidden cost: building text in a loop with + creates a new
// String on EVERY iteration. For 5 names that's wasteful; for
// 100,000 it is genuinely slow.
String report = "";
for (int i = 1; i <= 3; i++) {
report = report + "Line " + i + "\n"; // new String each loop
}
System.out.print(report);
}
}2️⃣ StringBuilder — One Buffer, Many Appends
StringBuilder is a mutable, growable buffer of characters. Instead of creating a new object on every change, append() writes into the same buffer — and it returns the builder itself, so calls can be chained.
StringBuilder sb = new StringBuilder();
sb.append("Hello").append(", ").append("World");
String result = sb.toString(); // "Hello, World"| Method | What it does |
|---|---|
| append(x) | Add x to the end (returns the builder) |
| insert(i, x) | Insert x at index i |
| delete(a, b) | Remove chars from a to b |
| reverse() | Reverse the buffer in place |
| length() | Current number of characters |
| toString() | Produce the final immutable String |
public class Main {
public static void main(String[] args) {
// StringBuilder is a MUTABLE, growable buffer of characters.
// append() changes the SAME object instead of creating new ones.
StringBuilder sb = new StringBuilder();
sb.append("Shopping list:").append("\n"); // calls can be chained
String[] items = {"Milk", "Eggs", "Bread"};
for (int i = 0; i < items.length; i++) {
sb.append(i + 1).append(". ").append(items[i]).append("\n");
}
// Other handy operations
sb.insert(0, ">>> "); // insert at a position
sb.append("Total items: ").append(items.length);
System.out.println("Length: " + sb.length());
// Convert back to a real String at the very end
String result = sb.toString();
System.out.println(result);
}
}3️⃣ String Formatting With printf and format
Glueing values together with + gets ugly fast, and you can't easily control decimals or alignment. Format specifiers solve that. System.out.printf prints directly; String.format returns the text as a value.
| Specifier | Inserts | Example |
|---|---|---|
| %s | Any value as text | "Alice" |
| %d | Whole number | 42 |
| %f | Decimal number | 19.500000 |
| %.2f | 2 decimal places | 19.50 |
| %5d | Right-justify width 5 | " 42" |
| %-10s | Left-justify width 10 | "Item " |
| %03d | Zero-pad to 3 digits | 009 |
| %,d | Thousands grouping | 1,240,000 |
| %% | Literal percent sign | % |
| %n | Newline (portable) | ↵ |
public class Main {
public static void main(String[] args) {
String name = "Alice";
int score = 9;
double price = 19.5;
// printf prints with placeholders; format() returns the String.
System.out.printf("%s scored %d points%n", name, score);
// %.2f -> 2 decimal places. %% prints a literal percent sign.
System.out.printf("Price: $%.2f (%.0f%% off)%n", price, 10.0);
// Width and alignment: %-10s left-justifies in a 10-char field.
System.out.printf("|%-10s|%5d|%n", "Item", 42);
System.out.printf("|%-10s|%5d|%n", "Widget", 7);
// Build a String instead of printing it
String label = String.format("[%03d] %s", score, name); // zero-pad
System.out.println(label);
// Comma grouping for big numbers
System.out.printf("Population: %,d%n", 1240000);
}
}public class Main {
public static void main(String[] args) {
// 🎯 YOUR TURN — replace each ___ using the hint beside it.
String[] parts = {"Learn", "Coding", "Fast"};
// 1) One buffer, edited in place — not a new String every loop turn.
StringBuilder sb = new ___(); // 👉 replace ___ with StringBuilder
for (String p : parts) {
// 2) Add to the end. It returns the builder, so calls chain.
sb.___(p).append(" "); // 👉 replace ___ with append
}
System.out.println("[" + sb.toString().trim() + "]");
// 3) Characters so far — a method here, unlike an array's length field.
System.out.println("Length: " + sb.___()); // 👉 replace ___ with length
// 4) Put something at a position instead of at the end.
sb.___(0, ">> "); // 👉 replace ___ with insert
System.out.println(sb.toString().trim());
// 5) Flip the whole buffer round.
sb.___(); // 👉 replace ___ with reverse
System.out.println(sb.toString().trim());
// Emptying it: cheaper than making a new builder.
sb.setLength(0);
sb.append("empty again");
System.out.println(sb);
}
}🧩 Reorder Challenge
Reorder these lines so they build a CSV-style line "a,b,c" with a StringBuilder and print it.
sb.append("a").append(",").append("b").append(",").append("c");
System.out.println(sb.toString());
StringBuilder sb = new StringBuilder();
String line = sb.toString();StringBuilder sb = new StringBuilder();
sb.append("a").append(",").append("b").append(",").append("c");
String line = sb.toString();
System.out.println(sb.toString());Why: The builder must be created first, then appended to, before toString() can extract the finished text. (The line variable and the printed sb.toString() are equivalent — you'd normally print line.) Output: a,b,c.
🧠 Quick Recall
1. What does this print?
String s = "abc";
s.toUpperCase();
System.out.println(s);abc — unchanged! Strings are immutable, so toUpperCase() returns a new String that is thrown away. You must capture it: s = s.toUpperCase();.
2. What does this print?
System.out.printf("%05.2f%n", 3.5);03.50. Two decimal places, total width 5, zero-padded — "3.50" is 4 chars, so one leading zero fills it to 5.
3. What does this print?
StringBuilder sb = new StringBuilder("Java");
sb.reverse();
System.out.println(sb);avaJ. reverse() mutates the builder in place, and printing it implicitly calls toString().
Common Beginner Mistakes
- ❌ Ignoring the return value: s.replace("a","b"); does nothing useful — assign it: s = s.replace(...).
- ❌ Concatenating in a big loop: use StringBuilder.append, not +, when the loop is large.
- ❌ Wrong specifier type: %d with a double, or %f with an int, throws an IllegalFormatConversionException.
- ❌ Forgetting %% : a lone % in a format string is an error; use %% for a literal percent.
- ❌ Never calling toString(): a StringBuilder is not a String; call toString() when you need one.
📋 Quick Reference
| Method / Specifier | Description | Example → result |
|---|---|---|
| new StringBuilder() | Create an empty buffer | StringBuilder sb = new StringBuilder(); |
| append(x) | Add to the end; returns the builder | sb.append("a").append("b") |
| insert(i, x) | Insert x at index i | sb.insert(0, ">> ") |
| delete(a, b) | Remove chars from a up to b | sb.delete(0, 2) |
| reverse() | Reverse the buffer in place | "Java" → "avaJ" |
| length() | Current number of characters | sb.length() |
| toString() | Produce the final immutable String | sb.toString() |
| %s | Any value as text | "Alice" |
| %d | Whole number | 42 |
| %.2f | Decimal, 2 places | 19.50 |
| %-10s | Left-justify in width 10 | "Item " |
| %03d | Zero-pad to 3 digits | 009 |
| %,d | Thousands grouping | 1,240,000 |
| %% / %n | Literal percent / portable newline | % / ↵ |
| String.format(fmt, ...) | Build a formatted String (vs printf prints it) | String.format("[%03d]", 9) → "[009]" |
🎉 Lesson Complete!
You now understand why Strings are immutable, when to reach for StringBuilder, and how to produce polished output with printf and String.format — including decimals, padding, alignment, and grouping.
Practice quiz
What does this print? StringBuilder sb = new StringBuilder("Java"); sb.reverse(); System.out.println(sb);
- Java
- JAVA
- avaJ
- java
Answer: avaJ. reverse() mutates the builder in place to avaJ, and printing it implicitly calls toString().
Why are Java Strings immutable a problem when concatenating in a large loop?
- Each + creates a new String object, making the loop O(n squared) work
- The loop never terminates
- Strings cannot be concatenated at all
- It causes a compile error
Answer: Each + creates a new String object, making the loop O(n squared) work. Because Strings are immutable, each + allocates a brand-new object. In a large loop that quadratic allocation is genuinely slow - StringBuilder fixes it.
What does new StringBuilder().append("a").append(1).append("b").toString() produce?
- "ab"
- "a b"
- "1ab"
- "a1b"
Answer: "a1b". append accepts any value and adds it to the end in order: "a", then 1, then "b" gives "a1b". Calls chain because append returns the builder.
What does System.out.printf("%05.2f%n", 3.5) print?
- 3.50
- 03.50
- 3.5
- 003.5
Answer: 03.50. Two decimal places gives "3.50" (4 chars); width 5 zero-padded adds one leading zero: 03.50.
What is the difference between System.out.printf and String.format?
- printf prints to the console; String.format returns the formatted String as a value
- They use different specifiers
- String.format is faster
- printf returns the String
Answer: printf prints to the console; String.format returns the formatted String as a value. Both use identical specifiers, but printf prints directly while String.format builds and returns the text so you can store or pass it.
What does this print? String s = "abc"; s.toUpperCase(); System.out.println(s);
- ABC
- Abc
- abc
- It throws an exception
Answer: abc. Strings are immutable: toUpperCase() returns a NEW String that is discarded. You must capture it with s = s.toUpperCase().
Which specifier prints a literal percent sign in a format string?
- %
- %%
- \%
- %p
Answer: %%. A lone % starts a specifier, so you write %% to print one literal percent sign.
What does String.format("[%03d]", 9) return?
- "[9]"
- "[ 9]"
- "[90]"
- "[009]"
Answer: "[009]". %03d zero-pads the number to 3 digits, producing "[009]".
When should you reach for StringBuilder instead of +?
- For every single concatenation, always
- When building a string piece by piece, especially inside a loop
- Never - + is always better
- Only for joining exactly two literals
Answer: When building a string piece by piece, especially inside a loop. Use StringBuilder when assembling text in a loop. For a one-off join of two or three literals, plain + is fine and more readable.
Which printf specifier should you prefer for a portable newline?
- %n
- %l
Answer: %n. %n produces a platform-independent newline inside printf, preferred over so output uses the correct line ending on every OS.
Continue this course
- Previous: The Math Class
- Next: ArrayList — The resizable list you'll use constantly — add, get, remove, loop
- Quick reference: Java cheat sheet
Frequently asked questions
Why are Java Strings immutable, and why does that matter?
Once a String object is created its characters can never change; any operation like concatenation or replace returns a NEW String and leaves the original untouched. Immutability makes Strings safe to share across threads and safe to use as HashMap keys, and it enables the String pool optimisation. The downside is that building text by repeated concatenation creates many throwaway objects, which is exactly what StringBuilder fixes.
When should I use StringBuilder instead of + concatenation?
Use StringBuilder when you build a string piece by piece, especially inside a loop. Each + on Strings allocates a new object, so concatenating in a large loop is O(n squared) work. StringBuilder appends into one growable buffer, making it linear. For a one-off join of two or three literals, plain + is perfectly fine and more readable.
What is the difference between System.out.printf and String.format?
They use identical format specifiers, but printf prints the result straight to the console and returns nothing useful, while String.format builds and returns the formatted String so you can store it, log it, or pass it elsewhere. Reach for String.format when you need the text as a value, and printf when you just want to display it.
What do %s, %d, and %.2f mean?
They are format specifiers. %s inserts any value as a String, %d inserts a whole number (int/long), and %f inserts a floating-point number. The .2 in %.2f means 'two digits after the decimal point'. You can add width and flags too: %-10s left-justifies in a 10-character field, %05d zero-pads to 5 digits, and %,d groups thousands with commas.
How do I print a literal percent sign or a new line in printf?
Write %% to print a single literal percent sign, because a lone % starts a format specifier. Use %n for a platform-independent newline (preferred over inside printf), so your output uses the correct line ending on every operating system.