Skip to content
Java strings 5 min read

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

FeatureStringStringBuffer
MutabilityImmutableMutable
Thread safetyThread-safe by nature (immutable)Thread-safe (synchronized methods)
PerformanceSlower for repeated modificationsFaster for repeated modifications
Memory usageCreates new objects on every changeReuses one internal buffer
StorageString Pool (literals) or heapHeap only
CharSequenceYesYes
Java versionAllJava 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. StringBuilder has 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 new String object backed by a copy of the buffer’s contents, so the returned String and the StringBuffer are independent afterward.

When to Use Which

SituationUse
Fixed, read-only textString
String used as a HashMap keyString (immutable keys are safe)
Building a string in a loopStringBuffer or StringBuilder
Shared mutable string across threadsStringBuffer
Building a string on a single threadStringBuilder (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.

Last updated June 13, 2026
Was this helpful?