String vs StringBuilder vs StringBuffer in Java (With Code)
String is immutable, StringBuilder is fast and mutable, StringBuffer is mutable and thread safe. Compare all three with compiled Java examples and a simple rule.

Say you write a loop that joins ten thousand words into one sentence using +=. It works on your laptop. Then your teacher tries a hundred thousand words and your program crawls. Nothing is wrong with the logic. The cause is how Java treats String.
Java gives you three classes for text: String, StringBuilder and StringBuffer. This post shows what separates them, with code that compiles, and a simple rule for choosing.
Use String for text that does not change. Use StringBuilder when you build or edit text in a loop. Use StringBuffer only when several threads must share one text object, which is rare. StringBuilder and StringBuffer have the same methods, and they differ in thread safety.
String vs StringBuilder vs StringBuffer: comparison table
| Point | String | StringBuilder | StringBuffer |
|---|---|---|---|
| Can you change it? | No (immutable) | Yes (mutable) | Yes (mutable) |
| Thread safe? | Yes, because it never changes | No | Yes (synchronized methods) |
| Speed for many edits | Slow | Fastest | Slower than StringBuilder |
| Stored in the string pool? | Literals are | No | No |
equals() compares | The text | The reference only | The reference only |
| Typical use | Names, keys, constants | Loops, building output | Shared text edited by threads |
The class pages in the Oracle Javadoc confirm the key facts: StringBuilder is described as a mutable sequence of characters with no synchronization guarantee, and StringBuffer is the thread-safe version.
String is immutable
Immutable means the object cannot change after it is created. A method such as concat does not edit the text. It returns a new String, and the old one stays as it was.
public class Immutable {
public static void main(String[] args) {
String s = "Dev";
s.concat("Hives"); // result thrown away
System.out.println(s); // Dev
s = s.concat("Hives"); // new object, reassigned
System.out.println(s); // DevHives
StringBuilder sb = new StringBuilder("Dev");
sb.append("Hives"); // same object changes
System.out.println(sb); // DevHives
}
}
The first print shows Dev, because the result of concat was never saved. This is one of the most common beginner mistakes. The StringBuilder at the bottom changes in place, so no reassignment is needed.
The string pool and why == surprises people
Java keeps a pool of string literals. Two literals with the same text point to the same object, while new String(...) forces a separate one. Always compare text with equals, not ==.
public class Pool {
public static void main(String[] args) {
String a = "java";
String b = "java";
String c = new String("java");
System.out.println(a == b); // true (same pooled object)
System.out.println(a == c); // false (different object)
System.out.println(a.equals(c)); // true (same text)
StringBuilder x = new StringBuilder("java");
StringBuilder y = new StringBuilder("java");
System.out.println(x.equals(y)); // false
System.out.println(x.toString().equals(y.toString())); // true
}
}
Note the last two lines. StringBuilder does not override equals, so two builders holding the same text are not equal. Convert both with toString() before comparing.
StringBuilder methods you will use
StringBuilder edits the same object again and again. The methods below cover most beginner needs.
public class Methods {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("hello");
sb.append(" world");
sb.insert(0, ">> ");
sb.reverse();
sb.reverse();
sb.setCharAt(3, 'H');
sb.deleteCharAt(sb.length() - 1);
System.out.println(sb); // >> Hello worl
System.out.println(sb.length()); // 13
String result = sb.toString();
System.out.println(result.toUpperCase());
}
}
The program prints >> Hello worl, then 13, then >> HELLO WORL. Call toString() at the end when you need a normal String back.
Create
new StringBuilder()or start from text.Edit
Use
append,insert,deleteorreverse.Finish
Call
toString()to get a String.
Speed: why += in a loop is slow
Each s += "a" creates a new String and copies all the old characters into it. Doing that many times copies the text again and again. StringBuilder keeps one growing buffer instead.
public class Speed {
public static void main(String[] args) {
int n = 100_000;
long t1 = System.nanoTime();
String s = "";
for (int i = 0; i < n; i++) {
s += "a";
}
long stringMs = (System.nanoTime() - t1) / 1_000_000;
long t2 = System.nanoTime();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append("a");
}
long builderMs = (System.nanoTime() - t2) / 1_000_000;
System.out.println("String += : " + stringMs + " ms, length " + s.length());
System.out.println("StringBuilder : " + builderMs + " ms, length " + sb.length());
}
}
I ran this on JDK 21. The String version took far longer than the StringBuilder version, by a large factor. Your times will differ with your computer, so run it yourself. The gap grows as the loop gets longer.
A single + in one statement, like "Hello " + name, is fine. The Java compiler handles it well. The problem is repeated concatenation inside a loop.
Thread safety: where StringBuffer fits
StringBuffer makes each method synchronized. That means only one thread can run a method on it at a time. StringBuilder skips that protection, so two threads writing to one builder can lose updates.
public class Threads {
public static void main(String[] args) throws InterruptedException {
StringBuilder builder = new StringBuilder();
StringBuffer buffer = new StringBuffer();
Runnable task = () -> {
for (int i = 0; i < 10_000; i++) {
builder.append("x");
buffer.append("x");
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Expected 20000");
System.out.println("StringBuffer : " + buffer.length());
System.out.println("StringBuilder : " + builder.length());
}
}
The StringBuffer length is always 20000. The StringBuilder length is often lower, and it can even throw an exception. In my run it printed 19237, but the number changes every time, because the loss depends on timing.
Even so, StringBuffer is rarely the best answer today. When threads share text, many programmers keep one builder per thread and join the results at the end, which avoids locking altogether.
Which one should you pick?
Pick StringBuilder when
- You build text in a loop
- Only one thread touches the text
- You need the fastest option
Pick StringBuffer when
- Several threads must write to the same object
- You maintain older code that already uses it
- Safety matters more than the small speed cost
For plain text that never changes, use String. It is the safest and the simplest. If you are working through the basics of Java in order, the free Java beginner syllabus shows where strings fit.
Interview answers in one line each
- Why is String immutable? Immutable objects are safe to share and reuse, which makes the string pool and use as map keys safe.
- Which is faster, StringBuilder or StringBuffer? StringBuilder, because it has no synchronization.
- How do you reverse a string?
new StringBuilder(s).reverse().toString(). - Does StringBuilder override equals? No. Compare with
toString().equals(...).
A real example: building a report line
Say you have a list of student names and you want one comma-separated line for a file. A loop with StringBuilder is the classic way. Java also offers String.join, which does the same job in one call and uses a builder inside.
import java.util.List;
public class Report {
public static void main(String[] args) {
List<String> names = List.of("Asha", "Rohan", "Meera");
StringBuilder sb = new StringBuilder();
for (int i = 0; i < names.size(); i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(names.get(i));
}
System.out.println(sb); // Asha, Rohan, Meera
System.out.println(String.join(", ", names)); // Asha, Rohan, Meera
}
}
Use String.join when you already have a list. Use the builder when each piece needs logic, such as skipping blanks or adding a prefix.
How a StringBuilder grows
A StringBuilder holds an internal array of characters. The Javadoc says the no-argument constructor starts with room for 16 characters. When you add more than fits, Java allocates a bigger array and copies the old text across. That copy is cheap compared with copying on every single append, but it is not free.
If you already know the rough size, tell the builder at the start. This avoids repeated growth when you are building very large text.
public class Capacity {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder(1000); // room for 1000 chars
System.out.println(sb.length()); // 0, nothing added yet
System.out.println(sb.capacity()); // 1000
sb.append("ready");
System.out.println(sb.length()); // 5
}
}
Length is how many characters you have added. Capacity is how many fit before the next growth. Beginners often mix the two up.
Common mistakes
- Ignoring the return value.
s.toUpperCase();alone does nothing. Save the result. - Using == on strings. It compares references, not text.
- Using += in a long loop. Switch to StringBuilder.
- Using StringBuffer everywhere. It adds locking cost that most programs never need.
What to do next
Copy the Speed program and run it on your own machine. Then change n to a smaller and a larger number and watch how the gap moves. Save your results in a README, using the layout from the GitHub README guide for beginners.
