algoz
Chapters/01 Java basics/StringBuilder

StringBuilder

Append, edit, and assemble text in mutable buffers without copying strings on every step.

Builds on
You'll gain
Build strings fast in loopswithout copying memory on every step
Practice
Sheet coming sooncoding problems for this note
Summary

A StringBuilder manages a mutable buffer of characters that lets you append, insert, and edit text in place without allocating a new String on every operation.

The idea in real life

Think of writing notes on a whiteboard instead of printing a new sheet of paper for every change. With paper, every time you want to add a word or fix a spelling mistake, you must throw away the entire sheet and print a brand-new page with all the old and new text combined. A whiteboard lets you write letters at the end, erase a few words in the middle, or wipe the board clean, all on the same physical surface. StringBuilder is that whiteboard for text in Java: it lets you add, remove, and alter characters in place without creating a brand-new string on every keystroke.

Why it exists

  1. String objects in Java are immutable; every time you concatenate text with the plus operator, Java allocates a brand-new String object and copies every character from the existing strings into the new one.
  2. Concatenating strings inside a loop that runs thousands of times produces thousands of short-lived objects and takes work proportional to the square of the character count, slowing programs down.
  3. StringBuilder maintains a resizable internal character buffer that updates in place, allowing appends, insertions, and deletions without allocating a new object on each step.
  4. Once all modifications finish, a single call to toString converts the internal buffer into an immutable String.

How it works

Why StringBuilder exists

Repeated string concatenation creates wasteful copies that slow down loops.

As introduced in strings, every String object in Java is immutable. Once constructed on the heap, the sequence of characters inside a String cannot be changed.

When you use the plus operator + to append text, Java cannot alter the existing string in place. Instead, it must allocate a brand-new String object with a larger character buffer and copy every single character from the old string into the new one.

In a loop that runs n times, this repeated copying becomes expensive:

  • The first pass copies 1 character.
  • The second pass copies 2 characters.
  • The third pass copies 3 characters.
  • The n-th pass copies n characters.

Across all n iterations, the total number of copied characters is $1 + 2 + 3 + \dots + n$, which equals $n(n + 1) / 2$. For 10,000 iterations, the program performs roughly 50 million character copy operations and creates 10,000 temporary string objects that immediately become garbage.

StringBuilder belongs to the java.lang package, so it is always available without an import statement. It manages a mutable array of characters that expands as needed. Appending a character writes directly into spare room in the existing buffer, avoiding intermediate allocations.

Main.java
public class Main {
    public static void main(String[] args) {
        String fixed = "Java";
        fixed = fixed + " 17";

        StringBuilder builder = new StringBuilder();
        builder.append("Java");
        builder.append(" 17");

        System.out.println("String result: " + fixed);
        System.out.println("StringBuilder result: " + builder.toString());
    }
}
Output
String result: Java 17
StringBuilder result: Java 17

In this program:

  • fixed is a String variable initially referencing "Java". The concatenation expression fixed + " 17" allocates a brand-new String object with the characters "Java 17" and updates fixed to point to it.
  • builder is a StringBuilder instance created using new StringBuilder().
  • Each call to builder.append(...) writes characters directly into the builder's mutable buffer without allocating a new object.
  • builder.toString() creates and returns a finished immutable String containing "Java 17".

Appending

Add text and primitive values to the end of the buffer.

The primary method for adding data to a StringBuilder is append. It appends the text representation of its argument to the end of the current buffer.

The StringBuilder class provides overloaded versions of append for all primitive types (int, long, boolean, char, float, double), as well as character arrays and strings. You do not need to convert numbers or booleans into strings manually before appending them.

Every append method returns a reference to the very same StringBuilder instance (this). Because the method returns itself, you can chain multiple method calls sequentially in a single statement.

Main.java
public class Main {
    public static void main(String[] args) {
        StringBuilder report = new StringBuilder();

        report.append("Task ")
              .append(101)
              .append(": status=")
              .append(true)
              .append(", score=")
              .append(94.5);

        System.out.println(report.toString());
    }
}
Output
Task 101: status=true, score=94.5

In this program:

  • report is an empty StringBuilder instance.
  • The chained statement calls append five consecutive times: appending the String "Task ", the int 101, the String ": status=", the boolean true, the String ", score=", and the double 94.5.
  • Each call appends its argument into the buffer and returns report, allowing the subsequent .append(...) call to execute immediately on the same object.

Editing

Insert, replace, delete, or reverse characters in place.

Unlike String, which offers no mutating methods, StringBuilder provides methods to edit characters directly within its buffer:

  • insert(int offset, ...): Inserts text or primitive values starting at index offset. Characters originally located at and after offset are shifted to the right to make room.
  • deleteCharAt(int index): Removes the single character at index. Characters to the right shift left by one position to fill the gap.
  • delete(int start, int end): Removes characters from index start inclusive up to index end exclusive.
  • setCharAt(int index, char ch): Overwrites the character at index with ch. This modifies the character in place without shifting other elements.
  • replace(int start, int end, String str): Replaces the character range from index start inclusive to end exclusive with the string str.
  • reverse(): Reverses the sequence of characters in the builder in place and returns this.
Main.java
public class Main {
    public static void main(String[] args) {
        StringBuilder builder = new StringBuilder("hello world");

        builder.setCharAt(0, 'H');
        System.out.println("After setCharAt: " + builder.toString());

        builder.insert(5, ",");
        System.out.println("After insert: " + builder.toString());

        builder.replace(7, 12, "Java");
        System.out.println("After replace: " + builder.toString());

        builder.delete(5, 6);
        System.out.println("After delete: " + builder.toString());

        builder.deleteCharAt(builder.length() - 1);
        System.out.println("After deleteCharAt: " + builder.toString());

        builder.reverse();
        System.out.println("After reverse: " + builder.toString());
    }
}
Output
After setCharAt: Hello world
After insert: Hello, world
After replace: Hello, Java
After delete: Hello Java
After deleteCharAt: Hello Jav
After reverse: vaJ olleH

In this program:

  • builder is initialised with the text "hello world".
  • setCharAt(0, 'H') overwrites the lowercase 'h' with 'H', producing "Hello world".
  • insert(5, ",") inserts a comma at index 5 directly after "Hello", shifting " world" right to produce "Hello, world".
  • replace(7, 12, "Java") replaces the five characters "world" starting at index 7 with "Java", producing "Hello, Java".
  • delete(5, 6) removes the comma at index 5, producing "Hello Java".
  • deleteCharAt(builder.length() - 1) removes the trailing character 'a', producing "Hello Jav".
  • reverse() reverses the entire character sequence in place, producing "vaJ olleH".

Turning it back into a String

Inspect characters and convert the finished buffer to an immutable String.

When you finish mutating the text buffer, call toString() to obtain an immutable String object.

In addition to converting back to String, StringBuilder supports inspection methods that mirror String:

  • length(): Returns the number of characters currently held in the builder.
  • charAt(int index): Returns the char at the specified zero-based index. If index is negative or greater than or equal to length(), Java throws an IndexOutOfBoundsException.
  • substring(int start) and substring(int start, int end): Return a newly allocated String containing the designated range of characters. Calling substring does not modify the builder.
Main.java
public class Main {
    public static void main(String[] args) {
        StringBuilder builder = new StringBuilder("Algoz");

        int characterCount = builder.length();
        char firstChar = builder.charAt(0);
        String prefix = builder.substring(0, 3);
        String finishedText = builder.toString();

        System.out.println("Length: " + characterCount);
        System.out.println("First character: " + firstChar);
        System.out.println("Substring: " + prefix);
        System.out.println("Final String: " + finishedText);
    }
}
Output
Length: 5
First character: A
Substring: Alg
Final String: Algoz

In this program:

  • characterCount receives 5 from builder.length().
  • firstChar retrieves the character 'A' at index 0.
  • prefix extracts the first three characters from index 0 inclusive to 3 exclusive as a new String "Alg".
  • finishedText holds the final immutable String "Algoz".

Going deeper

Capacity and growth

An oversized buffer grows automatically when content exceeds available room.

A StringBuilder maintains two distinct numeric measurements:

  • Length: The number of valid characters currently stored in the builder (builder.length()).
  • Capacity: The total number of characters the underlying array can hold before the builder must reallocate its buffer (builder.capacity()).

When you create a builder using the default constructor new StringBuilder(), Java allocates an initial buffer with a capacity of 16 characters.

You can specify a custom initial capacity using the constructor new StringBuilder(int capacity). If you construct a builder from an existing string with new StringBuilder(String str), the initial capacity defaults to the length of the string plus 16.

When appending characters causes length() to exceed capacity(), the builder automatically allocates a larger internal array and copies all existing characters into the new array. The old buffer is discarded.

While Java implementations expand the buffer to provide spare room (often roughly doubling the size plus a few extra characters), the exact growth rule is an internal JVM implementation detail rather than an API specification guarantee. Never rely on an exact capacity formula in your application logic.

If you know in advance approximately how many characters your text will contain, specify the capacity in the constructor. Pre-sizing the builder allocates the necessary memory once, eliminating intermediate buffer allocations and character copies.

Main.java
public class Main {
    public static void main(String[] args) {
        StringBuilder defaultBuilder = new StringBuilder();
        System.out.println("Default initial capacity: " + defaultBuilder.capacity());
        System.out.println("Default initial length: " + defaultBuilder.length());

        defaultBuilder.append("Algorithms");
        System.out.println("After appending word: length=" + defaultBuilder.length() + ", capacity=" + defaultBuilder.capacity());

        StringBuilder sizedBuilder = new StringBuilder(100);
        System.out.println("Custom initial capacity: " + sizedBuilder.capacity());
    }
}
Output
Default initial capacity: 16
Default initial length: 0
After appending word: length=10, capacity=16
Custom initial capacity: 100

In this program:

  • defaultBuilder starts with length 0 and default capacity 16.
  • Appending "Algorithms" (10 characters) increases length to 10 while capacity remains 16 because the characters fit into the initial buffer.
  • sizedBuilder is created with an explicit initial capacity of 100, preparing enough room for 100 characters without buffer reallocation.

Building output for many lines

Assemble lines of text in memory and print them in one call.

In how a Java program runs, we used System.out.println to display text.

Every call to System.out.println flushes text to standard output, invoking the operating system console stream. In competitive programming and DSA problems where a program outputs thousands of lines, executing thousands of separate System.out.println calls creates noticeable I/O delay.

A standard optimization is accumulating output lines into a single StringBuilder. Separate each line by appending the newline character '\n'. Once the loop completes, send the entire assembled text to standard output with a single call to System.out.print(builder.toString()).

Main.java
public class Main {
    public static void main(String[] args) {
        StringBuilder output = new StringBuilder();
        int totalRows = 4;

        for (int i = 1; i <= totalRows; i++) {
            output.append("Row ")
                  .append(i)
                  .append(": square = ")
                  .append(i * i)
                  .append('\n');
        }

        System.out.print(output.toString());
    }
}
Output
Row 1: square = 1
Row 2: square = 4
Row 3: square = 9
Row 4: square = 16

In this program:

  • output collects lines across all iterations of the for loop.
  • Each iteration appends the row label, index, squared calculation, and a trailing '\n'.
  • System.out.print prints the entire four-line block in a single terminal write operation. Note 12 covers input and output streams in depth.

Reversing and palindromes

Compare characters with two pointers to check palindromes without allocating memory.

A palindrome is a sequence of characters that reads the same forwards and backwards, such as "racecar".

The reverse() method on StringBuilder reverses a character sequence in place. You can test whether a string is a palindrome by loading it into a StringBuilder, reversing it, and comparing the result with the original string using equals.

While reverse() is convenient, it is not the most memory-efficient way to check for palindromes. Reversing with StringBuilder allocates a new builder object, copies every character into it, reverses the internal buffer, allocates a new String object via toString(), and then compares all characters.

In DSA problems, the preferred method uses two integer index pointers: one starting at the beginning (left = 0) and one at the end (right = text.length() - 1). The pointers move inward toward each other, comparing characters at each step. This approach allocates zero extra memory, stops immediately on the very first mismatch, and inspects at most half the characters.

Main.java
public class Main {
    static boolean isPalindromeWithReverse(String text) {
        StringBuilder builder = new StringBuilder(text);
        builder.reverse();
        return text.equals(builder.toString());
    }

    static boolean isPalindromeTwoPointers(String text) {
        int left = 0;
        int right = text.length() - 1;
        while (left < right) {
            if (text.charAt(left) != text.charAt(right)) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }

    public static void main(String[] args) {
        String word1 = "racecar";
        String word2 = "algoz";

        System.out.println("Using reverse on racecar: " + isPalindromeWithReverse(word1));
        System.out.println("Using two pointers on racecar: " + isPalindromeTwoPointers(word1));
        System.out.println("Using two pointers on algoz: " + isPalindromeTwoPointers(word2));
    }
}
Output
Using reverse on racecar: true
Using two pointers on racecar: true
Using two pointers on algoz: false

In this program:

  • isPalindromeWithReverse constructs a StringBuilder from text, calls reverse(), and compares the reversed string with the input.
  • isPalindromeTwoPointers checks matching characters from both ends using a while loop from loops, returning false as soon as a mismatch occurs without allocating memory.
  • For "racecar", both methods return true. For "algoz", isPalindromeTwoPointers detects that 'a' does not equal 'z' on the very first check and exits immediately.

StringBuilder versus StringBuffer

Use StringBuilder for single-threaded code to avoid synchronization overhead.

Java provides two mutable character buffer classes: StringBuffer and StringBuilder.

StringBuffer was introduced in Java 1.0. To permit safe use across concurrent threads, every public method on StringBuffer is declared with the synchronized modifier. Synchronization acquires and releases an internal monitor lock on every method call to ensure that two threads cannot modify the buffer at the same time.

Java 5 introduced StringBuilder as an unsynchronized drop-in replacement. It provides the exact same method signatures (append, insert, delete, reverse, toString) but omits thread locking.

In competitive programming, algorithmic problems, and standard application routines, text assembly executes entirely within a single thread. Because no other thread accesses the buffer, synchronization locks are unnecessary and add CPU overhead.

Multithreading and concurrency are outside the scope of this chapter. Always choose StringBuilder unless you are interfacing with legacy library APIs that explicitly demand a StringBuffer.

Main.java
public class Main {
    public static void main(String[] args) {
        StringBuilder builder = new StringBuilder();
        builder.append("Single").append("-threaded");

        StringBuffer buffer = new StringBuffer();
        buffer.append("Multi").append("-threaded");

        System.out.println("StringBuilder: " + builder.toString());
        System.out.println("StringBuffer: " + buffer.toString());
    }
}
Output
StringBuilder: Single-threaded
StringBuffer: Multi-threaded

In this program:

  • builder is a StringBuilder, running without thread synchronisation locks.
  • buffer is a StringBuffer, acquiring and releasing internal monitor locks on each operation.

Under the hood

Why appending is cheap on average

Occasional buffer reallocations are spread across many single-step writes.

When you append a character to a StringBuilder, one of two situations occurs:

  1. Spare room exists: The internal array has empty capacity. Writing the character takes a single write operation into array index length followed by incrementing length. This takes one step.
  2. Buffer is full: The array has no remaining capacity. The builder must allocate a new, larger array, copy all existing characters from the old array into the new one, and then insert the new character. Copying $k$ characters takes about $k$ steps.

Because the builder expands geometrically by a substantial factor each time it resizes, reallocations become exponentially less frequent as the buffer grows. The vast majority of appends execute in a single step into spare room.

When you sum the cost of all character copy operations across $n$ appends starting from an empty builder, the total copying work across all resizing steps combined is proportional to $n$. When averaged over all $n$ operations, the cost per append is about one step. This amortised efficiency gives StringBuilder its speed advantage over repeated string concatenation.

The following table records the exact capacity expansion sequence observed when appending single characters one by one to a default new StringBuilder() on OpenJDK 17.0.21 LTS:

EventCharacters stored (length())Buffer capacity (capacity())What happened inside the JVM
Initial allocation016Constructor allocated a 16-character buffer.
Append 16th character1616Buffer is completely full; no copying needed yet.
Append 17th character1734Capacity exceeded. Allocated a new buffer of 34 characters and copied 16 characters.
Append 34th character3434Buffer is completely full.
Append 35th character3570Capacity exceeded. Allocated a new buffer of 70 characters and copied 34 characters.
Append 70th character7070Buffer is completely full.
Append 71st character71142Capacity exceeded. Allocated a new buffer of 142 characters and copied 70 characters.
Append 142nd character142142Buffer is completely full.
Append 143rd character143286Capacity exceeded. Allocated a new buffer of 286 characters and copied 142 characters.

These numbers were measured by appending single characters to a default new StringBuilder() on OpenJDK 17.0.21 LTS. While modern HotSpot JDK implementations follow this sequence, the exact growth rule is an internal JVM implementation detail rather than an API specification guarantee.

Main.java
public class Main {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder(4);
        for (int i = 0; i < 12; i++) {
            sb.append('a');
        }
        System.out.println(sb.length() + " " + sb.capacity());
    }
}
Output
12 22

In this program:

  • sb starts with an explicit initial capacity of 4.
  • Appending the first 4 characters fills the initial buffer without copying.
  • Appending the 5th character exceeds capacity, triggering reallocation to a new buffer of capacity 10 (4 * 2 + 2) and copying 4 characters.
  • Appending the 11th character triggers a second reallocation to capacity 22 (10 * 2 + 2) and copies 10 characters.
  • When the loop finishes, sb.length() is 12 and sb.capacity() is 22.

Step through the figure below to watch how each append writes directly into spare room, and how a larger buffer is allocated and populated only when capacity is exhausted. Try changing capacity to 2 or appends to 6 in the header to observe different reallocation boundaries.

Equality

StringBuilder inherits identity equality from Object and does not compare characters.

Unlike String, which overrides the equals(Object obj) method to compare character sequences, StringBuilder does not override equals.

StringBuilder inherits the default equals implementation from java.lang.Object. This method tests reference identity (this == obj), checking whether two reference variables point to the exact same object in memory.

If you create two distinct StringBuilder objects containing identical text, first.equals(second) returns false.

To compare the textual contents of two StringBuilder instances:

  • Convert both builders to strings: first.toString().equals(second.toString()).
  • In Java 11 and newer, StringBuilder implements Comparable<StringBuilder>. You can call first.compareTo(second) == 0. This compares the character sequences lexicographically without allocating intermediate String objects.
Main.java
public class Main {
    public static void main(String[] args) {
        StringBuilder first = new StringBuilder("apple");
        StringBuilder second = new StringBuilder("apple");

        boolean objectEquals = first.equals(second);
        boolean stringEquals = first.toString().equals(second.toString());
        boolean compareToEquals = first.compareTo(second) == 0;

        System.out.println("first.equals(second): " + objectEquals);
        System.out.println("first.toString().equals(...): " + stringEquals);
        System.out.println("first.compareTo(second) == 0: " + compareToEquals);
    }
}
Output
first.equals(second): false
first.toString().equals(...): true
first.compareTo(second) == 0: true

In this program:

  • first and second are two separate objects on the heap, each holding "apple".
  • first.equals(second) evaluates to false because they are two different objects.
  • first.toString().equals(second.toString()) converts both to String and evaluates to true.
  • first.compareTo(second) == 0 compares the characters directly and evaluates to true.

Sharing a builder

Passing a builder to a method allows that method to mutate your text.

As taught in methods, Java passes all parameters by value. When you pass an object reference such as a StringBuilder into a method, Java copies the reference address into the method's parameter variable.

Both the caller's variable and the helper method's parameter point to the exact same StringBuilder instance on the heap.

Because StringBuilder is mutable, any mutating method called inside the helper routine—such as append, setCharAt, or reverse—modifies the caller's text buffer directly.

This behaviour contrasts sharply with String. Passing a String reference into a method is safe from unintended changes because String is immutable; the helper method cannot alter the caller's string.

Main.java
public class Main {
    static void appendSuffix(StringBuilder target) {
        target.append(" (processed)");
    }

    public static void main(String[] args) {
        StringBuilder document = new StringBuilder("Order #404");
        System.out.println("Before method call: " + document.toString());

        appendSuffix(document);
        System.out.println("After method call: " + document.toString());
    }
}
Output
Before method call: Order #404
After method call: Order #404 (processed)

In this program:

  • document is initialised with "Order #404".
  • appendSuffix receives the reference target, which points to the same object as document.
  • Calling target.append(" (processed)") mutates the underlying buffer.
  • When control returns to main, document.toString() displays "Order #404 (processed)".

String concatenation in bytecode

The compiler cannot merge repeated plus operations across loop iterations.

Understanding how Java compiles string concatenation clarifies why StringBuilder remains essential even when the plus operator + appears convenient.

When you concatenate string literals on a single line, such as String s = "a" + "b" + "c";, the Java compiler evaluates the expression at compile time and stores the folded literal "abc" directly in the class bytecode.

For non-constant string expressions on a single line, Java historical implementations differed:

  • In Java 8 and older, javac translated expressions like a + b + c into new StringBuilder().append(a).append(b).append(c).toString().
  • In Java 9 and newer (under JEP 280), javac translates single-line string concatenations using the invokedynamic bytecode instruction calling StringConcatFactory.makeConcatWithConstants. This delegates strategy selection to the JVM runtime.

However, inside a loop:

Java
String text = "";
for (int i = 0; i < n; i++) {
    text += i;
}

The compiler cannot merge independent loop iterations into a single builder. On every pass of the loop, the program must evaluate string concatenation anew, allocating a brand-new string object and copying all characters accumulated so far.

Disassembling a test class compiled on JDK 17 using javap -c displays the exact bytecode difference between concatenating in a loop and using StringBuilder:

text
  public static java.lang.String concatInLoop(int);
    Code:
       0: ldc           #7                  // String ""
       2: astore_1
       3: iconst_0
       4: istore_2
       5: iload_2
       6: iload_0
       7: if_icmpge     24
      10: aload_1
      11: iload_2
      12: invokedynamic #9,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;
      17: astore_1
      18: iinc          2, 1
      21: goto          5
      24: aload_1
      25: areturn

  public static java.lang.String concatBuilder(int);
    Code:
       0: new           #13                 // class java/lang/StringBuilder
       3: dup
       4: invokespecial #15                 // Method java/lang/StringBuilder."<init>":()V
       7: astore_1
       8: iconst_0
       9: istore_2
      10: iload_2
      11: iload_0
      12: if_icmpge     27
      15: aload_1
      16: iload_2
      17: invokevirtual #16                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      20: pop
      21: iinc          2, 1
      24: goto          10
      27: aload_1
      28: invokevirtual #20                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      31: areturn

This disassembly was produced using javap -c on OpenJDK 17.0.21 LTS; the // comments were generated automatically by javap.

In concatInLoop, instruction 12 executes invokedynamic on every single pass, allocating a new string each time. In concatBuilder, instruction 0 creates new StringBuilder once before the loop begins, instruction 17 calls append inside the loop, and instruction 28 invokes toString only once after the loop finishes.

Compact strings representation

Modern Java stores Latin-1 characters in single bytes to cut memory use.

In Java 8 and earlier, String and StringBuilder stored characters internally in a char[] array. Because Java characters use UTF-16 encoding, every single character consumed 16 bits (2 bytes) of memory, even when storing standard ASCII text where the high 8 bits were always zero.

Starting in Java 9 (under JEP 254: Compact Strings), Java changed the internal storage of both String and StringBuilder to a byte array (byte[] value) accompanied by a byte encoding flag (byte coder).

The JVM inspects the characters stored in the builder:

  • LATIN1 encoding (coder = 0): If every character has a code point value from 0 to 255 (covering standard ASCII and Western European characters), each character is stored in a single byte (8 bits).
  • UTF16 encoding (coder = 1): If any character has a code point greater than 255, the JVM switches the buffer to UTF-16 encoding, using 2 bytes (16 bits) per character.

This internal optimization reduces memory consumption by half for standard Latin-1 strings while operating transparently behind the public StringBuilder API.

In Java

Here is a complete program that brings these concepts together: formatting a number with comma separators, cleaning an input phrase in a single pass with a pre-sized builder, checking palindrome status using a memory-efficient two-pointer routine, and assembling a final report.

In this program:

  • formatWithCommas accepts an integer number and returns a String with commas separating groups of three digits.
  • sanitize accepts a String input and returns a lowercase alphanumeric String with all punctuation and spaces removed.
  • isPalindrome accepts a String text and returns a boolean indicating whether the text reads identically forwards and backwards without allocating memory.
  • main runs each helper method and outputs the formatted and verified results.
Main.java
public class Main {
    // Formats an integer with commas as thousand separators.
    static String formatWithCommas(int number) {
        if (number == 0) {
            return "0";
        }
        StringBuilder builder = new StringBuilder();
        int temp = Math.abs(number);
        int digitCount = 0;

        while (temp > 0) {
            if (digitCount > 0 && digitCount % 3 == 0) {
                builder.append(',');
            }
            int lastDigit = temp % 10;
            builder.append(lastDigit);
            temp /= 10;
            digitCount++;
        }

        if (number < 0) {
            builder.append('-');
        }

        builder.reverse();
        return builder.toString();
    }

    // Removes all punctuation and spaces, keeping only alphanumeric characters in lowercase.
    static String sanitize(String input) {
        StringBuilder cleaned = new StringBuilder(input.length());
        for (int i = 0; i < input.length(); i++) {
            char ch = input.charAt(i);
            if ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')) {
                cleaned.append(ch);
            } else if (ch >= 'A' && ch <= 'Z') {
                cleaned.append((char) (ch + 32));
            }
        }
        return cleaned.toString();
    }

    // Checks whether the string reads the same forwards and backwards without allocating memory.
    static boolean isPalindrome(String text) {
        int left = 0;
        int right = text.length() - 1;
        while (left < right) {
            if (text.charAt(left) != text.charAt(right)) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }

    public static void main(String[] args) {
        int sampleNumber = 12345678;
        String formatted = formatWithCommas(sampleNumber);
        System.out.println("Formatted number: " + formatted);

        String rawPhrase = "A man, a plan, a canal: Panama!";
        String cleanedPhrase = sanitize(rawPhrase);
        System.out.println("Cleaned phrase: " + cleanedPhrase);

        boolean palindromeResult = isPalindrome(cleanedPhrase);
        System.out.println("Is palindrome: " + palindromeResult);

        StringBuilder summary = new StringBuilder();
        summary.append("Length: ").append(cleanedPhrase.length());
        summary.append(", Verified: ").append(palindromeResult);
        System.out.println(summary.toString());
    }
}
Output
Formatted number: 12,345,678
Cleaned phrase: amanaplanacanalpanama
Is palindrome: true
Length: 21, Verified: true

Traps

  1. Using equals to compare two StringBuilder objects

    StringBuilder does not override equals from java.lang.Object. Calling sb1.equals(sb2) evaluates reference identity (==), returning false even when both builders hold identical characters. Compare their contents by calling toString().equals(...) on both, or in Java 11 and newer, test sb1.compareTo(sb2) == 0.

    ✕ WRONGif (sb1.equals(sb2)) { ... }
    ✓ RIGHTif (sb1.toString().equals(sb2.toString())) { ... }
  2. String concatenation inside append calls

    Writing greeting + " " + name evaluates string concatenation before the argument reaches append(). This allocates a temporary String object on every call, defeating the purpose of using StringBuilder. Chain separate append() calls instead.

    ✕ WRONGbuilder.append(greeting + " " + name);
    ✓ RIGHTbuilder.append(greeting).append(' ').append(name);
  3. Assuming reverse returns a new independent builder

    The reverse() method mutates the existing builder in place and returns this. If you store the return value in rev expecting sb to remain unchanged, both variables point to the exact same reversed buffer on the heap. Pass the original builder into a new StringBuilder before reversing if you need both copies.

    ✕ WRONGStringBuilder rev = sb.reverse();
    ✓ RIGHTStringBuilder rev = new StringBuilder(sb).reverse();
  4. Calling deleteCharAt inside a forward loop

    Deleting a character with deleteCharAt(i) shifts all subsequent characters left by one position. The character originally at index i + 1 moves into index i, but the loop increment i++ advances past it, skipping that character completely. In addition, shifting repeatedly takes work proportional to the square of the character count; a better approach is building a new StringBuilder that appends only matching characters.

    ✕ WRONGfor (int i = 0; i < sb.length(); i++) { if (sb.charAt(i) == ' ') sb.deleteCharAt(i); }
    ✓ RIGHTfor (int i = sb.length() - 1; i >= 0; i--) { if (sb.charAt(i) == ' ') sb.deleteCharAt(i); }
  5. Calling insert(0, ...) inside a loop

    Calling insert(0, ch) shifts every existing character in the buffer to the right by one position to make room at the front. Doing this inside a loop of n iterations copies characters on every step, scaling quadratically. Append characters to the end instead and call reverse() once, or iterate backward.

    ✕ WRONGfor (int i = 0; i < text.length(); i++) builder.insert(0, text.charAt(i));
    ✓ RIGHTfor (int i = text.length() - 1; i >= 0; i--) builder.append(text.charAt(i));
  6. Passing a char literal to the StringBuilder constructor for initial content

    StringBuilder does not have a constructor that accepts a char. Because char promotes to int through numeric promotion, passing 'A' invokes the capacity constructor StringBuilder(int capacity) with the ASCII value 65. This creates an empty builder with an initial capacity of 65 characters instead of a builder containing the letter "A". Pass a String literal "A" or append the character after construction.

    ✕ WRONGStringBuilder builder = new StringBuilder('A');
    ✓ RIGHTStringBuilder builder = new StringBuilder("A");
  7. Forgetting toString where a String is expected

    StringBuilder is a mutable buffer class, not a String. Java is statically typed and does not automatically convert a StringBuilder reference to a String. You must explicitly call .toString() to extract the immutable string from the builder.

    ✕ WRONGString message = builder;
    ✓ RIGHTString message = builder.toString();
  8. Not pre-sizing the builder when the total length is known

    The default constructor allocates an initial buffer of only 16 characters. If you know you are assembling a string of several thousand characters, leaving the builder at default capacity forces the JVM to allocate new arrays and copy characters multiple times as it expands. Specifying an estimated capacity allocates memory once.

    ✕ WRONGStringBuilder builder = new StringBuilder();
    ✓ RIGHTStringBuilder builder = new StringBuilder(estimatedCapacity);
AlgozSet in Geist & Geist MonoJava track