String vs StringBuffer
Choosing between String and StringBuffer is one of the most common decisions you will make when writing Java programs. The short answer: use String for fixed text you will not change, and use StringBuffer when you need to build or modify a string repeatedly — especially in multithreaded code.
The Core Difference: Mutability
A String in Java is immutable — once created, its content can never change. Every operation that appears to “modify” a String actually produces a brand-new object.
A StringBuffer, on the other hand, is mutable — it wraps a resizable internal character array that you can append to, insert into, delete from, and reverse, all without allocating a new object each time.
// String — a new object is created on every "change"
String s = "Hello";
s += ", World"; // s now points to a NEW String object
s += "!"; // yet another new String object
// StringBuffer — the same object is mutated in place
StringBuffer sb = new StringBuffer("Hello");
sb.append(", World"); // same object, same heap location
sb.append("!");
System.out.println(sb.toString()); // Hello, World!
Output:
Hello, World!
Note: See Why String is Immutable for a deep dive into the design reasons behind
String’s immutability (security, caching, and thread safety for literals).
Side-by-Side Comparison
| Feature | String | StringBuffer |
|---|---|---|
| Mutability | Immutable | Mutable |
| Thread safety | Thread-safe by nature (immutable) | Thread-safe (synchronized methods) |
| Performance | Slower for repeated modifications | Faster for repeated modifications |
| Memory usage | Creates new objects on every change | Reuses one internal buffer |
| Storage | String Pool (literals) or heap | Heap only |
CharSequence | Yes | Yes |
| Java version | All | Java 1.0+ |
Performance: Why It Matters in Loops
The performance difference becomes dramatic when you do many concatenations. Every + on a String inside a loop allocates a temporary object that the garbage collector must eventually clean up.
// BAD: creates thousands of intermediate String objects
String result = "";
for (int i = 0; i < 10_000; i++) {
result += i; // a new String per iteration
}
// GOOD: one StringBuffer, one buffer
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 10_000; i++) {
sb.append(i); // mutates in place
}
String result = sb.toString();
Tip: Even outside a loop, if you are building a string from more than two or three pieces,
StringBuffer(or StringBuilder) is the right tool.
Thread Safety
StringBuffer was designed for concurrent use. Every public method — append(), insert(), delete(), reverse(), and more — is declared synchronized. This means only one thread can modify the buffer at a time, preventing data corruption.
StringBuffer shared = new StringBuffer();
Runnable task = () -> {
for (int i = 0; i < 5; i++) {
shared.append("X");
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(shared.length()); // always 10 — no race condition
Output:
10
String is also safe across threads, but for a different reason — because it can never be modified at all, there is nothing to synchronize.
Warning: If you only need mutable strings on a single thread, prefer StringBuilder over
StringBuffer.StringBuilderhas an identical API but skips synchronization, making it noticeably faster in single-threaded scenarios.
Common API — What They Share
Both String and StringBuffer implement CharSequence, so they share a common set of read-only operations.
String s = "Java";
StringBuffer sb = new StringBuffer("Java");
System.out.println(s.length()); // 4
System.out.println(sb.length()); // 4
System.out.println(s.charAt(0)); // J
System.out.println(sb.charAt(0)); // J
System.out.println(s.substring(1, 3)); // av
System.out.println(sb.substring(1, 3)); // av
Output:
4
4
J
J
av
av
What Only StringBuffer Can Do
Because StringBuffer is mutable, it offers modification methods that String simply does not have.
StringBuffer sb = new StringBuffer("Hello World");
// Append
sb.append("!");
System.out.println(sb); // Hello World!
// Insert
sb.insert(5, ",");
System.out.println(sb); // Hello, World!
// Delete
sb.delete(5, 6);
System.out.println(sb); // Hello World!
// Replace
sb.replace(6, 11, "Java");
System.out.println(sb); // Hello Java!
// Reverse
sb.reverse();
System.out.println(sb); // !avaJ olleH
Output:
Hello World!
Hello, World!
Hello World!
Hello Java!
!avaJ olleH
Converting Between String and StringBuffer
You will often need to move back and forth between the two types.
// String → StringBuffer
String str = "OpenSource";
StringBuffer sb = new StringBuffer(str);
// StringBuffer → String
String result = sb.toString();
System.out.println(result); // OpenSource
Note:
toString()allocates a newStringobject backed by a copy of the buffer’s contents, so the returnedStringand theStringBufferare independent afterward.
When to Use Which
| Situation | Use |
|---|---|
| Fixed, read-only text | String |
String used as a HashMap key | String (immutable keys are safe) |
| Building a string in a loop | StringBuffer or StringBuilder |
| Shared mutable string across threads | StringBuffer |
| Building a string on a single thread | StringBuilder (faster than StringBuffer) |
| Method return values (most cases) | String |
Under the Hood
How String Stores Characters
In Java 9+, the JVM uses compact strings by default: if all characters fit in Latin-1 (byte values 0–255), the String stores them as a byte[] rather than a char[], cutting memory in half for typical ASCII text. A one-byte coder flag records which encoding is in use. This is completely transparent to your code.
How StringBuffer Stores Characters
StringBuffer maintains a char[] with some extra capacity. The default initial capacity is 16 characters. When the buffer fills up, it reallocates to (currentCapacity * 2) + 2 characters and copies the contents over. You can pre-size the buffer to avoid these reallocations:
// Pre-size when you know the approximate final length
StringBuffer sb = new StringBuffer(256);
The + Operator Under the Hood
Before Java 9, the compiler transformed "a" + "b" + "c" into a series of StringBuilder operations. From Java 9 onward, the JVM uses invokedynamic with StringConcatFactory, choosing the most efficient strategy at runtime. Either way, repeated + inside a loop still creates a new builder each iteration — which is why you should hoist it out manually.
Synchronization Cost
Each synchronized method in StringBuffer acquires the object’s intrinsic lock. In a contended scenario with many threads, this can become a bottleneck. For high-throughput single-threaded work, StringBuilder removes this overhead entirely.
Related Topics
- StringBuffer — full reference for all
StringBuffermethods and capabilities - StringBuilder — the faster, non-synchronized sibling of
StringBuffer - StringBuffer vs StringBuilder — picking the right mutable string class
- Why String is Immutable — the design reasoning behind Java’s immutable
String - String Pool & intern() — how the JVM reuses
Stringliterals to save memory - Garbage Collection Deep-Dive — understand the heap pressure that excess
Stringobjects create