01 · Java basics — note 11 of 12
StringBuilder
Append, edit, and assemble text in mutable buffers without copying strings on every step.
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
Stringobjects in Java are immutable; every time you concatenate text with the plus operator, Java allocates a brand-newStringobject and copies every character from the existing strings into the new one.- 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.
StringBuildermaintains a resizable internal character buffer that updates in place, allowing appends, insertions, and deletions without allocating a new object on each step.- Once all modifications finish, a single call to
toStringconverts the internal buffer into an immutableString.
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 copiesncharacters.
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.
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());
}
}String result: Java 17
StringBuilder result: Java 17In this program:
fixedis aStringvariable initially referencing"Java". The concatenation expressionfixed + " 17"allocates a brand-newStringobject with the characters"Java 17"and updatesfixedto point to it.builderis aStringBuilderinstance created usingnew 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 immutableStringcontaining"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.
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());
}
}Task 101: status=true, score=94.5In this program:
reportis an emptyStringBuilderinstance.- The chained statement calls
appendfive consecutive times: appending theString"Task ", theint101, theString": status=", thebooleantrue, theString", score=", and thedouble94.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 indexoffset. Characters originally located at and afteroffsetare shifted to the right to make room.deleteCharAt(int index): Removes the single character atindex. Characters to the right shift left by one position to fill the gap.delete(int start, int end): Removes characters from indexstartinclusive up to indexendexclusive.setCharAt(int index, char ch): Overwrites the character atindexwithch. This modifies the character in place without shifting other elements.replace(int start, int end, String str): Replaces the character range from indexstartinclusive toendexclusive with the stringstr.reverse(): Reverses the sequence of characters in the builder in place and returnsthis.
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());
}
}After setCharAt: Hello world
After insert: Hello, world
After replace: Hello, Java
After delete: Hello Java
After deleteCharAt: Hello Jav
After reverse: vaJ olleHIn this program:
builderis 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 thecharat the specified zero-based index. Ifindexis negative or greater than or equal tolength(), Java throws anIndexOutOfBoundsException.substring(int start)andsubstring(int start, int end): Return a newly allocatedStringcontaining the designated range of characters. Callingsubstringdoes not modify the builder.
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);
}
}Length: 5
First character: A
Substring: Alg
Final String: AlgozIn this program:
characterCountreceives 5 frombuilder.length().firstCharretrieves the character'A'at index 0.prefixextracts the first three characters from index 0 inclusive to 3 exclusive as a newString"Alg".finishedTextholds the final immutableString"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.
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());
}
}Default initial capacity: 16
Default initial length: 0
After appending word: length=10, capacity=16
Custom initial capacity: 100In this program:
defaultBuilderstarts 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. sizedBuilderis 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()).
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());
}
}Row 1: square = 1
Row 2: square = 4
Row 3: square = 9
Row 4: square = 16In this program:
outputcollects lines across all iterations of theforloop.- Each iteration appends the row label, index, squared calculation, and a trailing
'\n'. System.out.printprints 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.
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));
}
}Using reverse on racecar: true
Using two pointers on racecar: true
Using two pointers on algoz: falseIn this program:
isPalindromeWithReverseconstructs aStringBuilderfromtext, callsreverse(), and compares the reversed string with the input.isPalindromeTwoPointerschecks matching characters from both ends using awhileloop from loops, returningfalseas soon as a mismatch occurs without allocating memory.- For
"racecar", both methods returntrue. For"algoz",isPalindromeTwoPointersdetects 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.
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());
}
}StringBuilder: Single-threaded
StringBuffer: Multi-threadedIn this program:
builderis aStringBuilder, running without thread synchronisation locks.bufferis aStringBuffer, 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:
- Spare room exists: The internal array has empty capacity. Writing the character takes a single write operation into array index
lengthfollowed by incrementinglength. This takes one step. - 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:
| Event | Characters stored (length()) | Buffer capacity (capacity()) | What happened inside the JVM |
|---|---|---|---|
| Initial allocation | 0 | 16 | Constructor allocated a 16-character buffer. |
| Append 16th character | 16 | 16 | Buffer is completely full; no copying needed yet. |
| Append 17th character | 17 | 34 | Capacity exceeded. Allocated a new buffer of 34 characters and copied 16 characters. |
| Append 34th character | 34 | 34 | Buffer is completely full. |
| Append 35th character | 35 | 70 | Capacity exceeded. Allocated a new buffer of 70 characters and copied 34 characters. |
| Append 70th character | 70 | 70 | Buffer is completely full. |
| Append 71st character | 71 | 142 | Capacity exceeded. Allocated a new buffer of 142 characters and copied 70 characters. |
| Append 142nd character | 142 | 142 | Buffer is completely full. |
| Append 143rd character | 143 | 286 | Capacity 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.
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());
}
}12 22In this program:
sbstarts 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 andsb.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,
StringBuilderimplementsComparable<StringBuilder>. You can callfirst.compareTo(second) == 0. This compares the character sequences lexicographically without allocating intermediateStringobjects.
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);
}
}first.equals(second): false
first.toString().equals(...): true
first.compareTo(second) == 0: trueIn this program:
firstandsecondare two separate objects on the heap, each holding"apple".first.equals(second)evaluates tofalsebecause they are two different objects.first.toString().equals(second.toString())converts both toStringand evaluates totrue.first.compareTo(second) == 0compares the characters directly and evaluates totrue.
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.
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());
}
}Before method call: Order #404
After method call: Order #404 (processed)In this program:
documentis initialised with"Order #404".appendSuffixreceives the referencetarget, which points to the same object asdocument.- 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 + cintonew StringBuilder().append(a).append(b).append(c).toString(). - In Java 9 and newer (under JEP 280), javac translates single-line string concatenations using the
invokedynamicbytecode instruction callingStringConcatFactory.makeConcatWithConstants. This delegates strategy selection to the JVM runtime.
However, inside a loop:
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:
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: areturnThis 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:
formatWithCommasaccepts an integernumberand returns aStringwith commas separating groups of three digits.sanitizeaccepts aStringinputand returns a lowercase alphanumericStringwith all punctuation and spaces removed.isPalindromeaccepts aStringtextand returns abooleanindicating whether the text reads identically forwards and backwards without allocating memory.mainruns each helper method and outputs the formatted and verified results.
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());
}
}Formatted number: 12,345,678
Cleaned phrase: amanaplanacanalpanama
Is palindrome: true
Length: 21, Verified: trueTraps
Using equals to compare two StringBuilder objects
StringBuilderdoes not overrideequalsfromjava.lang.Object. Callingsb1.equals(sb2)evaluates reference identity (==), returningfalseeven when both builders hold identical characters. Compare their contents by callingtoString().equals(...)on both, or in Java 11 and newer, testsb1.compareTo(sb2) == 0.✕ WRONGif (sb1.equals(sb2)) { ... }✓ RIGHTif (sb1.toString().equals(sb2.toString())) { ... }String concatenation inside append calls
Writing
greeting + " " + nameevaluates string concatenation before the argument reachesappend(). This allocates a temporaryStringobject on every call, defeating the purpose of usingStringBuilder. Chain separateappend()calls instead.✕ WRONGbuilder.append(greeting + " " + name);✓ RIGHTbuilder.append(greeting).append(' ').append(name);Assuming reverse returns a new independent builder
The
reverse()method mutates the existing builder in place and returnsthis. If you store the return value inrevexpectingsbto remain unchanged, both variables point to the exact same reversed buffer on the heap. Pass the original builder into a newStringBuilderbefore reversing if you need both copies.✕ WRONGStringBuilder rev = sb.reverse();✓ RIGHTStringBuilder rev = new StringBuilder(sb).reverse();Calling deleteCharAt inside a forward loop
Deleting a character with
deleteCharAt(i)shifts all subsequent characters left by one position. The character originally at indexi + 1moves into indexi, but the loop incrementi++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 newStringBuilderthat 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); }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 ofniterations copies characters on every step, scaling quadratically. Append characters to the end instead and callreverse()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));Passing a char literal to the StringBuilder constructor for initial content
StringBuilderdoes not have a constructor that accepts achar. Becausecharpromotes tointthrough numeric promotion, passing'A'invokes the capacity constructorStringBuilder(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 aStringliteral"A"or append the character after construction.✕ WRONGStringBuilder builder = new StringBuilder('A');✓ RIGHTStringBuilder builder = new StringBuilder("A");Forgetting toString where a String is expected
StringBuilderis a mutable buffer class, not aString. Java is statically typed and does not automatically convert aStringBuilderreference to aString. You must explicitly call.toString()to extract the immutable string from the builder.✕ WRONGString message = builder;✓ RIGHTString message = builder.toString();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);