StringBuilder and StringBuffer in Java
Joining 10,000 strings with s += part inside a loop means creating 10,000 new String objects and copying everything accumulated so far every single time. The code works, but it slows down quadratically as the data grows. That is exactly the job StringBuilder exists for.
StringBuilder in Java is a mutable sequence of characters from the java.lang package: its methods append(), insert(), delete(), replace() and reverse() change the contents of the same object instead of creating a new string. StringBuffer is its thread-safe (synchronized) counterpart. Because java.lang is imported automatically, you never need an import statement for StringBuilder. The immutable String class and its own methods are a separate topic and are not repeated here.
String vs StringBuilder vs StringBuffer
String is immutable: every "modification" produces a new object. StringBuilder and StringBuffer are mutable and change their contents in place. The only difference between the two is that StringBuffer methods are synchronized (thread-safe) and StringBuilder methods are not, so in single-threaded code StringBuilder is faster and is the default choice.
| Feature | String | StringBuilder | StringBuffer |
|---|---|---|---|
| Mutability | Immutable | Mutable | Mutable |
| Thread safety | Yes (because it is immutable) | No | Yes, methods are synchronized |
| Speed of repeated changes | Slow — a new object every time | Fast | Slightly slower than StringBuilder due to locking |
| Introduced in | Java 1.0 | Java 5 | Java 1.0 |
equals() compares content | Yes | No (compares references) | No (compares references) |
| When to use | Values that never change, HashMap keys, constants | Building a string in a loop on one thread — almost always | The same object is really modified by several threads; legacy APIs |
All three classes are declared final and implement the CharSequence interface, so many JDK methods accept any of them.
Easy to get wrong
StringBuffer synchronization protects only a single method call. The sequence if (sb.length() > 0) sb.append(", "); executed from two threads can still race: another thread may change the buffer between the check and the append. Compound operations need external synchronization — or, more simply, give each thread its own StringBuilder.
When to use StringBuilder: concatenation in a loop
Use StringBuilder when a string is assembled piece by piece in a loop or across several places in your code. The += operator in a loop creates a new String on every iteration and copies all the accumulated text into it, while StringBuilder appends characters to one and the same buffer.
public class ConcatInLoop {
public static void main(String[] args) {
// Bad: a new string is created on every iteration
String s = "";
for (int i = 0; i < 5; i++) {
s += i;
}
System.out.println(s);
// Good: one object, data is appended to the buffer
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
sb.append(i);
}
System.out.println(sb.toString());
}
}
// Output:
// 01234
// 01234 The result is the same, but with 100,000 iterations the += version copies roughly n²/2 characters, while StringBuilder copies a linear amount.
For a single expression such as "Hello, " + name + "!" you do not need an explicit StringBuilder. Up to and including Java 8 the compiler turned such + into a chain of StringBuilder.append() calls; since Java 9 (JEP 280) it uses invokedynamic and StringConcatFactory, which performs at least as well as hand-written code. That is why IntelliJ IDEA flags this kind of code with the inspection "StringBuilder can be replaced with String": inside one expression, a readable + is better. In a loop, however, the compiler optimization does not help — there you need StringBuilder explicitly. All the ways to join strings with +, concat() and friends are compared in the lesson Java String Concatenation; to join ready-made strings with a delimiter, String.join() is usually the shortest option.
Creating a StringBuilder: constructors, length() and capacity()
A StringBuilder is created with new using one of four constructors. The object has two different "sizes": length() is how many characters the string currently holds, and capacity() is the size of the internal buffer, which grows automatically once it is full. StringBuilder has no size() method.
| Constructor | Contents | Initial capacity() |
|---|---|---|
StringBuilder() | Empty string | 16 |
StringBuilder(int capacity) | Empty string | The given number; negative — NegativeArraySizeException |
StringBuilder(String str) | Copy of str | str.length() + 16; null — NullPointerException |
StringBuilder(CharSequence seq) | Copy of seq (for example, another StringBuilder) | seq.length() + 16 |
public class StringBuilderExample1 {
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder("Hello");
System.out.println("string = " + stringBuilder);
System.out.println("length = " + stringBuilder.length());
System.out.println("capacity = " + stringBuilder.capacity());
StringBuilder empty = new StringBuilder();
System.out.println("capacity = " + empty.capacity());
empty.append("12345678901234567"); // 17 characters do not fit into 16
System.out.println("length = " + empty.length());
System.out.println("capacity = " + empty.capacity());
}
}
// Output:
// string = Hello
// length = 5
// capacity = 21
// capacity = 16
// length = 17
// capacity = 34 When the buffer overflows it roughly doubles (old capacity * 2 + 2, but never less than required), and the old data is copied into the new array. If you know the final size of the string in advance, pass it to the constructor — new StringBuilder(1000) — to avoid intermediate copies.
What does this code print?
new StringBuilder('a').append("bc") prints bc, not abc. There is no constructor that takes a char: the character 'a' is widened to the int 97, and StringBuilder(int capacity) is called — an empty object with room for 97 characters. To start with a character, write new StringBuilder("a") or new StringBuilder().append('a').
Java StringBuilder methods: cheat sheet
StringBuilder methods fall into two groups: those that modify the object (append, insert, delete, replace, reverse, setCharAt, setLength) and those that only read it (charAt, indexOf, substring, length, toString). Modifying methods, except setCharAt and setLength, return the same StringBuilder, so calls can be chained. StringBuffer has the same set of methods.
| Method | What it does | Returns | Edge case |
|---|---|---|---|
append(x) | Adds the string representation of any value to the end | this | A null reference is appended as the text "null" |
insert(offset, x) | Inserts a value before position offset | this | offset > length() — StringIndexOutOfBoundsException |
delete(start, end) | Removes characters from start to end - 1 | this | end > length() is clamped to the length, no exception |
deleteCharAt(index) | Removes one character | this | Invalid index — StringIndexOutOfBoundsException |
replace(start, end, str) | Replaces a range with a string of any length | this | start > length() — exception; end is clamped |
reverse() | Reverses the character sequence | this | Surrogate pairs (emoji) are kept intact |
charAt(index) | Returns the character at an index | char | Invalid index — StringIndexOutOfBoundsException |
setCharAt(index, ch) | Replaces the character at an index | void | Invalid index — StringIndexOutOfBoundsException |
setLength(n) | Truncates the string or pads it with '\u0000' | void | setLength(0) clears the object; negative n — exception |
length() / capacity() | Number of characters / buffer size | int | capacity() >= length() always |
indexOf(str), lastIndexOf(str) | Search for a substring | int | Not found — -1; accept only String, not char |
substring(start, end) | Returns part of the string | String | The StringBuilder itself is not changed |
toString() | Creates a new String with the current contents | String | Later changes to the builder do not affect that string |
compareTo(other) | Compares contents lexicographically (Java 11+) | int | 0 — contents are equal |
repeat(str, count) | Appends a string or character count times (Java 21+) | this | count < 0 — IllegalArgumentException |
A repeat() example (Java 21): new StringBuilder().repeat("=", 10) produces ========== — handy for separators in console output. The full list of overloads is in the official StringBuilder Javadoc.
The append() method
The append() method adds the string representation of the given value to the end of a StringBuilder: String, char, int, double, boolean, Object and so on. It returns the same object, so calls are conveniently chained, and at the end you get the string with toString().
public class AppendExample {
public static void main(String[] args) {
int a = 42;
StringBuilder sb = new StringBuilder(40);
String s = sb.append("a = ").append(a).append('!').toString();
System.out.println(s);
String name = null;
StringBuilder greeting = new StringBuilder("Hello, ");
greeting.append(name);
System.out.println(greeting);
// greeting.append(null); // compile error: reference to append is ambiguous
}
}
// Output:
// a = 42!
// Hello, null A variable holding null does not throw — the text "null" is appended. A bare null literal, however, does not compile: several overloads match at once (append(String), append(StringBuffer), append(char[])), and the compiler cannot pick one.
The insert() method
The insert(offset, value) method inserts a value before the character at index offset, shifting the rest of the string to the right. Java has no separate prepend() method: adding to the beginning of a string is done with insert(0, ...).
public class InsertExample {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("I Java!");
sb.insert(2, "like ");
System.out.println(sb);
sb.insert(0, ">> "); // prepend
System.out.println(sb);
sb.insert(sb.length(), 21); // inserting at the end works like append
System.out.println(sb);
sb.insert(100, "x"); // offset greater than length
}
}
// Output:
// I like Java!
// >> I like Java!
// >> I like Java!21
// Exception in thread "main" java.lang.StringIndexOutOfBoundsException: ... A valid offset is from 0 to length() inclusive. Keep in mind that inserting at the beginning of a long string shifts every character, so repeated insert(0, ...) in a loop is as slow as +=; it is better to append() and call reverse() once, or to assemble the parts in a different order.
charAt(), setCharAt() and setLength()
The charAt(index) method returns the character at an index (counting from zero), setCharAt(index, ch) replaces it directly inside the object, and setLength(n) truncates the string to n characters. Unlike String, where replacing a single character means building a new string, here the change happens in place.
public class SetCharAtExample {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello");
System.out.println(sb);
System.out.println("charAt(1) = " + sb.charAt(1));
sb.setCharAt(1, 'i');
System.out.println(sb);
sb.setLength(2);
System.out.println(sb);
sb.setLength(0); // clear
System.out.println("length = " + sb.length());
}
}
// Output:
// Hello
// charAt(1) = e
// Hillo
// Hi
// length = 0 If you pass setLength() a number greater than the current length, the string is padded with invisible '\u0000' characters, not spaces — on screen it looks like "nothing", but length() grows.
delete() and deleteCharAt()
The delete(start, end) method removes characters from index start inclusive to end exclusive, and deleteCharAt(index) removes a single character. If end is greater than the string length, it is silently clamped to length(), but an invalid start throws StringIndexOutOfBoundsException.
public class DeleteExample {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("This is a test.");
sb.delete(4, 7);
System.out.println("After delete: " + sb);
sb.deleteCharAt(0);
System.out.println("After deleteCharAt: " + sb);
sb.delete(3, 100); // end greater than length - no exception
System.out.println("After delete(3, 100): " + sb);
}
}
// Output:
// After delete: This a test.
// After deleteCharAt: his a test.
// After delete(3, 100): his A handy trick is removing a trailing separator after a loop: sb.setLength(sb.length() - 2) or sb.delete(sb.length() - 2, sb.length()) for a ", " tail. Check that the string is not empty first, otherwise you get an exception.
The replace() method
The replace(start, end, str) method replaces the characters from start to end - 1 with the string str, and its length may differ from the length of the replaced fragment. This is a replacement by position, not by content: unlike String.replace(), StringBuilder does not search for the substring itself.
public class ReplaceExample {
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder("This is a simple test.");
stringBuilder.replace(8, 16, "the final"); // "a simple" -> "the final"
System.out.println("After replace: " + stringBuilder);
}
}
// Output:
// After replace: This is the final test. To replace a fragment by its text, first find its position with indexOf(), then call replace(pos, pos + old.length(), newText).
The reverse() method
The reverse() method reverses the character sequence and returns the same object. It is the shortest way to reverse a string in Java, but remember: the StringBuilder itself is changed, no copy is created.
public class ReverseExample {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("abcdef");
String reversed = sb.reverse().toString();
System.out.println(reversed);
System.out.println(sb); // the original object is reversed too
}
}
// Output:
// fedcba
// fedcba Characters outside the Basic Multilingual Plane (emoji, for example) are stored as surrogate pairs of two char values; reverse() treats such a pair as one unit and does not break the character.
Searching and converting StringBuilder to String: indexOf(), substring(), toString()
The indexOf(), lastIndexOf() and substring() methods of StringBuilder work the same way as in String and do not modify the object. The toString() method converts a StringBuilder to a String: it creates a new immutable string with the current contents.
public class SearchExample {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Java StringBuilder Java");
System.out.println(sb.indexOf("Java"));
System.out.println(sb.lastIndexOf("Java"));
System.out.println(sb.indexOf("Java", 1));
System.out.println(sb.substring(5, 18));
System.out.println(sb.length()); // substring did not change the object
String result = sb.toString();
sb.append("!");
System.out.println(result); // the string is independent of later changes
}
}
// Output:
// 0
// 19
// 19
// StringBuilder
// 23
// Java StringBuilder Java StringBuilder's indexOf() has no char overload: sb.indexOf('J') does not compile, you have to write sb.indexOf("J"). How toString() works for objects in general and how to override it in your own classes is covered in the lesson toString() method in Java.
Comparing StringBuilders: equals(), compareTo() and contentEquals()
StringBuilder and StringBuffer do not override the equals() and hashCode() methods — they inherit the implementation from Object, which compares references. Two builders with the same text are not equal according to equals().
public class StringBuilderEquals {
public static void main(String[] args) {
StringBuilder sb1 = new StringBuilder("Hello");
StringBuilder sb2 = new StringBuilder("Hello");
System.out.println(sb1.equals(sb2));
System.out.println(sb1 == sb2);
// Correct ways to compare the contents:
System.out.println(sb1.toString().equals(sb2.toString()));
System.out.println(sb1.compareTo(sb2) == 0); // Java 11+
System.out.println("Hello".contentEquals(sb1)); // String vs StringBuilder
}
}
// Output:
// false
// false
// true
// true
// true Since Java 11, StringBuilder and StringBuffer implement Comparable, so they can be compared with compareTo() and sorted. The result follows the same rule as String.compareTo(): a negative number, zero or a positive number. Because there is no content-based equals()/hashCode(), a StringBuilder must not be used as a HashMap key or a HashSet element — store sb.toString() instead.
Where developers get tripped up
Most StringBuilder bugs come from treating it like a String: expecting content comparison, expecting a copy after a change, or missing the int overload. Here are five common mistakes that show up both in production code and in interviews:
- Comparing with
equals().sb1.equals(sb2)compares references and returnsfalseeven for identical text. UsecompareTo()ortoString().equals()(see the comparison section). new StringBuilder('a'). The character becomes anint, and you get an empty builder with capacity 97 (see the constructors section).- Expecting a copy after
reverse(),append(),delete(). These methods change the original object. If you still need the original text, savetoString()before the change or make a copy withnew StringBuilder(sb). - Creating the StringBuilder inside the loop.
for (...) { StringBuilder sb = new StringBuilder(); ... }defeats the purpose: the object should be created once, before the loop. - A StringBuilder for a single expression.
new StringBuilder().append(a).append(b).toString()instead ofa + bonly makes the code harder to read: the compiler already optimizes concatenation within one expression.
Frequently Asked Questions
Do I need to import StringBuilder in Java?
No. StringBuilder and StringBuffer live in the java.lang package, which is imported into every source file automatically. The line import java.lang.StringBuilder; is legal but redundant, and your IDE will mark it as unused.
How do I convert a StringBuilder to a String and back?
From StringBuilder to String — sb.toString() or String.valueOf(sb). From String to StringBuilder — the constructor new StringBuilder(str). toString() creates a new string on every call, so in a loop call it once, after you finish building. Example — in the section indexOf(), substring() and toString().
How do I clear a StringBuilder to reuse it?
Call sb.setLength(0): the length becomes zero while the already allocated buffer is kept, which saves memory when you fill it again. sb.delete(0, sb.length()) does the same. Creating a new object with new StringBuilder() is also fine if the old one is not referenced anywhere else.
Is StringBuffer deprecated? Can I use StringBuilder in a multithreaded application?
StringBuffer is not marked @Deprecated, but new code rarely needs it. In a multithreaded application StringBuilder is safe as long as the object lives inside one method or one thread — which is the most common case. You need StringBuffer only when the very same object is modified by several threads at once.
How do I get the size of a StringBuilder: size(), length() or capacity()?
StringBuilder has no size() method. The number of characters is returned by length(), while capacity() shows the size of the internal buffer, which is always at least the length. Spare buffer space can be released with trimToSize().
Comments