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

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"
MethodWhat 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.

SpecifierInsertsExample
%sAny value as text"Alice"
%dWhole number42
%fDecimal number19.500000
%.2f2 decimal places19.50
%5dRight-justify width 5" 42"
%-10sLeft-justify width 10"Item "
%03dZero-pad to 3 digits009
%,dThousands grouping1,240,000
%%Literal percent sign%
%nNewline (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

📋 Quick Reference

Method / SpecifierDescriptionExample → result
new StringBuilder()Create an empty bufferStringBuilder sb = new StringBuilder();
append(x)Add to the end; returns the buildersb.append("a").append("b")
insert(i, x)Insert x at index isb.insert(0, ">> ")
delete(a, b)Remove chars from a up to bsb.delete(0, 2)
reverse()Reverse the buffer in place"Java" → "avaJ"
length()Current number of characterssb.length()
toString()Produce the final immutable Stringsb.toString()
%sAny value as text"Alice"
%dWhole number42
%.2fDecimal, 2 places19.50
%-10sLeft-justify in width 10"Item "
%03dZero-pad to 3 digits009
%,dThousands grouping1,240,000
%% / %nLiteral 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

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.