Java StringBuffer
Java StringBuffer
Java has three classes for working with strings: String, StringBuilder, and StringBuffer. Two of them — StringBuilder and StringBuffer — look almost identical. They have the same methods, the same mutable buffer design, and even the same API. The difference is a single word: synchronised.
StringBuffer was introduced in Java 1.0 as the mutable alternative to the immutable String. Every method in StringBuffer is synchronised — meaning only one thread can execute it at a time. This thread safety came at a performance cost. When Java 1.5 introduced StringBuilder as an unsynchronised alternative, most single-threaded use cases migrated to StringBuilder. But StringBuffer still has its place — any time a single mutable string object must be safely shared and modified across multiple threads.
What Is StringBuffer?
StringBuffer is a thread-safe, mutable sequence of characters in java.lang. Like StringBuilder, it maintains an internal character buffer that grows as content is added. Unlike StringBuilder, every method that reads or modifies the buffer carries the synchronized keyword — the JVM enforces that only one thread can be inside any of these methods at a time.
StringBuffer internal structure:
StringBuffer sb = new StringBuffer("Hello");
┌─────────────────────────────────────────────┐
│ StringBuffer object │
│ │
│ char[] value = ['H','e','l','l','o', , , ]│ ← internal buffer
│ int count = 5 │ ← current length
│ int capacity = 21 │ ← buffer size
│ │
│ All methods: synchronized │
│ Only ONE thread can enter at a time │
└─────────────────────────────────────────────┘
Thread 1 calling append("!") ──► acquires lock ──► appends ──► releases lock
Thread 2 calling append("?") ──► waits for lock ──► appends ──► releases lock
(never interleaves)
The synchronisation is per-method. If two threads call append() concurrently, one waits for the other to finish. This prevents character interleaving and corrupt internal state — the two main risks when multiple threads write to the same buffer simultaneously.
Creating a StringBuffer
// File: StringBufferCreation.java
public class StringBufferCreation {
public static void main(String[] args) {
// Way 1 — empty buffer (default capacity = 16)
StringBuffer sb1 = new StringBuffer();
System.out.println("Empty capacity : " + sb1.capacity()); // 16
System.out.println("Empty length : " + sb1.length()); // 0
// Way 2 — with initial capacity
StringBuffer sb2 = new StringBuffer(128);
System.out.println("128 capacity : " + sb2.capacity()); // 128
// Way 3 — with initial content
StringBuffer sb3 = new StringBuffer("DevStackFlow");
System.out.println("Content : " + sb3);
System.out.println("Content length : " + sb3.length()); // 12
System.out.println("Content cap : " + sb3.capacity()); // 16 + 12 = 28
// Way 4 — from a CharSequence (String, StringBuilder, etc.)
CharSequence cs = "Learning Java";
StringBuffer sb4 = new StringBuffer(cs);
System.out.println("From CharSeq : " + sb4);
System.out.println();
// Capacity grows automatically when content exceeds it
StringBuffer sb5 = new StringBuffer(4);
sb5.append("Hello World!"); // 12 chars > 4 capacity
System.out.println("Auto capacity : " + sb5.capacity()); // (4+2)*2 = 12 or more
System.out.println("Content : " + sb5);
}
}Output:
Empty capacity : 16
Empty length : 0
128 capacity : 128
Content : DevStackFlow
Content length : 12
Content cap : 28
From CharSeq : Learning Java
Auto capacity : 12
Content : Hello World!
The capacity behaviour is identical to StringBuilder — starts at 16 by default, grows to (oldCapacity * 2) + 2 when content exceeds it.
Core Methods of StringBuffer
StringBuffer shares all the same methods as StringBuilder. Every one of them is synchronised.
append()
// File: StringBufferAppendDemo.java
public class StringBufferAppendDemo {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
// append() — accepts all primitive and reference types
sb.append("Order ID : ");
sb.append("ORD-2024-001");
sb.append("\n");
sb.append("Amount : Rs.");
sb.append(1299.50); // double
sb.append("\n");
sb.append("Quantity : ");
sb.append(3); // int
sb.append("\n");
sb.append("Paid : ");
sb.append(true); // boolean
System.out.println(sb.toString());
System.out.println();
// Method chaining — append returns the same StringBuffer
StringBuffer header = new StringBuffer()
.append("=".repeat(40))
.append("\n")
.append(" INVOICE SUMMARY")
.append("\n")
.append("=".repeat(40));
System.out.println(header);
}
}Output:
Order ID : ORD-2024-001
Amount : Rs.1299.5
Quantity : 3
Paid : true
========================================
INVOICE SUMMARY
========================================
insert(), delete(), replace(), reverse()
// File: StringBufferModifyDemo.java
public class StringBufferModifyDemo {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer("Hello World");
// insert(offset, value) — inserts at position
sb.insert(5, ",");
System.out.println("After insert : " + sb); // Hello, World
// delete(start, end) — removes a range
sb.delete(0, 7);
System.out.println("After delete : " + sb); // World
// deleteCharAt(index) — removes one character
sb.deleteCharAt(sb.length() - 1);
System.out.println("After delChar : " + sb); // Worl
// replace(start, end, str) — replaces a range
sb.replace(0, 4, "Java");
System.out.println("After replace : " + sb); // Java
// reverse() — reverses the buffer content
sb.reverse();
System.out.println("After reverse : " + sb); // avaJ
System.out.println();
// setCharAt(index, char) — modifies a single character
StringBuffer word = new StringBuffer("java");
word.setCharAt(0, 'J');
System.out.println("After setCharAt: " + word); // Java
// indexOf and lastIndexOf
StringBuffer text = new StringBuffer("abcabcabc");
System.out.println("indexOf('abc') : " + text.indexOf("abc")); // 0
System.out.println("indexOf('abc', 1) : " + text.indexOf("abc", 1)); // 3
System.out.println("lastIndexOf('abc') : " + text.lastIndexOf("abc")); // 6
}
}Output:
After insert : Hello, World
After delete : World
After delChar : Worl
After replace : Java
After reverse : avaJ
After setCharAt: Java
indexOf('abc') : 0
indexOf('abc', 1) : 3
lastIndexOf('abc') : 6
Thread Safety — What It Actually Means
The synchronized keyword on every StringBuffer method means the JVM locks the object before any thread can execute the method. This section demonstrates the problem StringBuffer solves.
// File: ThreadSafetyDemo.java
public class ThreadSafetyDemo {
public static void main(String[] args) throws InterruptedException {
// Test 1 — StringBuilder shared across threads (UNSAFE)
StringBuilder unsafeBuilder = new StringBuilder();
Runnable appendToSB = () -> {
for (int i = 0; i < 1000; i++) {
unsafeBuilder.append("A"); // no synchronisation
}
};
Thread t1 = new Thread(appendToSB);
Thread t2 = new Thread(appendToSB);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println("StringBuilder (unsafe):");
System.out.println(" Expected length: 2000");
System.out.println(" Actual length: " + unsafeBuilder.length()); // may vary!
System.out.println();
// Test 2 — StringBuffer shared across threads (SAFE)
StringBuffer safeBuffer = new StringBuffer();
Runnable appendToSBF = () -> {
for (int i = 0; i < 1000; i++) {
safeBuffer.append("A"); // synchronised — safe
}
};
Thread t3 = new Thread(appendToSBF);
Thread t4 = new Thread(appendToSBF);
t3.start(); t4.start();
t3.join(); t4.join();
System.out.println("StringBuffer (thread-safe):");
System.out.println(" Expected length: 2000");
System.out.println(" Actual length: " + safeBuffer.length()); // always 2000
}
}Output:
StringBuilder (unsafe):
Expected length: 2000
Actual length: 1893 ← unpredictable — may differ each run
StringBuffer (thread-safe):
Expected length: 2000
Actual length: 2000 ← always correct
StringBuilder's length may be less than 2000 because two threads can read the same length value, both compute the same next write position, and overwrite each other's character — a classic race condition. StringBuffer's lock prevents this: only one thread appends at a time, so every "A" is written correctly.
StringBuffer vs StringBuilder vs String — Comparison Table
| Aspect | String | StringBuilder | StringBuffer |
|---|---|---|---|
| Mutable | No — every operation creates new String | Yes — modifies in place | Yes — modifies in place |
| Thread-safe | Yes — immutability makes it inherently safe | No — not synchronised | Yes — every method is synchronized |
| Performance | Slowest in loops — creates many objects | Fastest — no synchronisation overhead | Slower than StringBuilder — sync lock overhead |
| When to use | Fixed values, constants, comparisons | Building strings in a single thread | Building strings shared across multiple threads |
+ operator | Yes — compiles to StringBuilder internally | No — use append() | No — use append() |
| Method return type | New String — immutable result | this — same object, chainable | this — same object, chainable |
| Synchronised | N/A — immutable | No | Yes — every method |
| Memory use | Many short-lived objects in loops | One buffer, one final String | One buffer, one final String |
| Introduced | Java 1.0 | Java 1.5 | Java 1.0 |
| Package | java.lang | java.lang | java.lang |
| Null append | N/A | Appends "null" | Appends "null" |
| Capacity default | N/A | 16 | 16 |
When to Choose StringBuffer Over StringBuilder
The decision comes down to one question: is this buffer accessed by more than one thread at the same time?
Single-threaded (most common case):
Method generates a report: Use StringBuilder
┌─────────────────────────────────┐
│ void generateReport() { │
│ StringBuilder sb = new ... │ ← local variable, never shared
│ sb.append(data); │
│ return sb.toString(); │
│ } │
└─────────────────────────────────┘
Multi-threaded (StringBuffer needed):
Shared log buffer for all threads: Use StringBuffer
┌─────────────────────────────────┐
│ class Logger { │
│ StringBuffer log = │ ← instance field, shared
│ new StringBuffer(); │ by multiple threads
│ │
│ void write(String msg) { │
│ log.append(msg); │ ← multiple threads call this
│ } │
│ } │
└─────────────────────────────────┘
// File: WhenToUseDemo.java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class WhenToUseDemo {
// Single-threaded use — StringBuilder is correct
public static String buildReport(String[] items, double[] prices) {
StringBuilder sb = new StringBuilder(); // local — not shared
sb.append("REPORT\n").append("=".repeat(20)).append("\n");
for (int i = 0; i < items.length; i++) {
sb.append(String.format("%-15s Rs.%.2f%n", items[i], prices[i]));
}
return sb.toString();
}
// Multi-threaded use — StringBuffer is necessary
static class SharedAuditLog {
private final StringBuffer log = new StringBuffer(); // shared across threads
public void write(String threadName, String message) {
// StringBuffer ensures no corruption from concurrent writes
log.append("[").append(threadName).append("] ")
.append(message).append("\n");
}
public String getLog() {
return log.toString();
}
}
public static void main(String[] args) throws InterruptedException {
// Single-threaded report
String[] items = {"Laptop", "Mouse", "Keyboard"};
double[] prices = {45999.0, 599.0, 1299.0};
System.out.println(buildReport(items, prices));
// Multi-threaded audit log
SharedAuditLog auditLog = new SharedAuditLog();
ExecutorService pool = Executors.newFixedThreadPool(3);
String[] threadNames = {"OrderService", "PaymentService", "InventoryService"};
for (String name : threadNames) {
pool.submit(() -> {
for (int i = 1; i <= 3; i++) {
auditLog.write(Thread.currentThread().getName(),
"Event " + i + " from " + name);
try { Thread.sleep(10); } catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
}
pool.shutdown();
pool.awaitTermination(5, TimeUnit.SECONDS);
System.out.println("=== Shared Audit Log (all 9 entries) ===");
System.out.println("Total entries: " +
auditLog.getLog().split("\n").length);
System.out.println("Log length is non-zero: " +
(auditLog.getLog().length() > 0));
}
}Output:
REPORT
====================
Laptop Rs.45999.00
Mouse Rs.599.00
Keyboard Rs.1299.00
=== Shared Audit Log (all 9 entries) ===
Total entries: 9
Log length is non-zero: true
buildReport uses StringBuilder — it is a local variable never seen by another thread. SharedAuditLog uses StringBuffer — three threads write to the same buffer concurrently, and synchronisation prevents corruption.
Basic Example
public class Main {
public static void main(String[] args) throws InterruptedException {
StringBuffer buffer = new StringBuffer();
// Two threads append to the SAME StringBuffer at the same time
Runnable appendTask = () -> {
for (int i = 0; i < 1000; i++) {
buffer.append("x");
}
};
Thread t1 = new Thread(appendTask);
Thread t2 = new Thread(appendTask);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Expected length: 2000");
System.out.println("Actual length : " + buffer.length());
}
}Output
Expected length: 2000 Actual length : 2000
How it works:
- ›
bufferis a singleStringBuffershared by two threads. - ›Both threads run
appendTask, each callingappend("x")1000 times. - ›Because every
StringBuffermethod issynchronized, only one thread can executeappend()at a time — the other waits its turn. - ›This guarantees no characters are lost or overwritten, so the final length is always exactly
2000, no matter how the threads interleave.
Best Practices
Use StringBuilder by default — switch to StringBuffer only when you confirm shared multi-thread access. The synchronisation overhead of StringBuffer is unnecessary in single-threaded code. Local variables in a method are never shared between threads. Instance fields accessed from multiple threads are the primary case for StringBuffer.
Consider synchronized StringBuilder or ReentrantLock as alternatives to StringBuffer. If you need fine-grained control — locking only specific sections, not every method call — wrapping StringBuilder in explicit synchronized blocks gives you more control than StringBuffer's method-level synchronisation. For complex multi-threaded string building, java.util.concurrent tools often offer better designs.
Always call toString() once at the end. Every toString() call creates a new String from the buffer content. Calling it inside a loop or multiple times in one build sequence creates unnecessary intermediate objects. Build completely, then convert once.
Prefer String.join() or streams for assembling known collections. For building comma-separated lists or joining items with a delimiter from a known collection, String.join(", ", list) is cleaner than a StringBuffer loop. Use StringBuffer when the building is iterative, conditional, or sequential — not for simple joins.
Common Mistakes
Mistake 1 — Using StringBuffer When StringBuilder Is Sufficient
// Unnecessary synchronisation overhead
public String buildUserProfile(String name, String email, int age) {
StringBuffer sb = new StringBuffer(); // local variable — never shared between threads
sb.append("Name: ").append(name)
.append(" | Email: ").append(email)
.append(" | Age: ").append(age);
return sb.toString();
}
// Correct — no threads share this local variable
public String buildUserProfile(String name, String email, int age) {
StringBuilder sb = new StringBuilder(); // no sync overhead needed
sb.append("Name: ").append(name)
.append(" | Email: ").append(email)
.append(" | Age: ").append(age);
return sb.toString();
}Mistake 2 — Assuming StringBuffer Makes the Entire Operation Atomic
// StringBuffer makes each METHOD CALL thread-safe
// But a SEQUENCE OF CALLS is NOT atomic
StringBuffer sb = new StringBuffer("Hello");
// Thread 1 calls:
int len = sb.length(); // gets 5 — safe
// Thread 2 appends " World" here — sb is now "Hello World", length 11
sb.insert(len, "!"); // inserts at position 5 — but sb is now 11 chars long
// Result might be "Hello! World" or "Hello World!" depending on timing
// If you need a sequence of operations to be atomic, use explicit synchronisation:
synchronized (sb) {
int length = sb.length();
sb.insert(length, "!");
}Each individual method call is synchronised. A sequence of calls — length() followed by insert() — is not atomic unless you add external synchronisation around the sequence.
Mistake 3 — Calling toString() Inside a Loop
StringBuffer sb = new StringBuffer();
for (String item : items) {
sb.append(item).append("\n");
System.out.println(sb.toString()); // creates a new String every iteration — wasteful
}
// Better — print only at the end, or print the StringBuilder directly in debug mode
for (String item : items) {
sb.append(item).append("\n");
}
System.out.println(sb); // println calls toString() once — acceptableMistake 4 — Using StringBuffer for String Constants
// Wasteful — a simple String constant does not need a mutable buffer
public static final StringBuffer PREFIX = new StringBuffer("APP_");
// Correct — constants are immutable by nature, use String
public static final String PREFIX = "APP_";Interview Questions
Q1. What is StringBuffer in Java and how is it different from StringBuilder?
StringBuffer is a thread-safe, mutable sequence of characters introduced in Java 1.0. Every method in StringBuffer — append(), insert(), delete(), replace(), reverse() — is declared synchronized, meaning only one thread can execute any of these methods on the same object at a time. StringBuilder, introduced in Java 1.5, has identical functionality but without synchronisation. The practical guideline: use StringBuilder for single-threaded string building (most cases) and StringBuffer when the same buffer object is shared and modified by multiple threads simultaneously.
Q2. Why was StringBuilder introduced when StringBuffer already existed?
When Java 1.0 introduced StringBuffer, thread safety was built in by default. As Java applications grew more complex, developers recognised that the vast majority of string building happens within a single thread — local variables in a method, response builders in a service layer, query constructors. These never need synchronisation. StringBuffer's mandatory locking on every method call was an unnecessary performance overhead for these single-threaded cases. Java 1.5 introduced StringBuilder as the unsynchronised alternative, letting developers choose the right tool: StringBuilder for performance in single-threaded code, StringBuffer where thread safety is genuinely needed.
Q3. Is StringBuffer completely thread-safe for all operations?
Each individual method call is thread-safe — the synchronized keyword ensures one thread at a time. However, a sequence of calls is not atomically thread-safe. If Thread A calls sb.length() and then sb.insert(length, value), Thread B could execute between those two calls and change the buffer's length. The result of Thread A's insert would then be at a position that no longer reflects the buffer state it read. For compound operations — read-then-modify sequences — callers must add explicit synchronisation around the entire sequence using synchronized (sb) { ... }.
Q4. When would you use StringBuffer over other alternatives in modern Java?
StringBuffer is appropriate when a single mutable String object is genuinely shared and concurrently modified by multiple threads — shared in-memory log buffers, concurrent message builders, shared response aggregators in multi-threaded systems. In most modern Java code, alternatives are preferred: StringBuilder for single-threaded work, String.join() or streams for assembling collections, and ConcurrentLinkedDeque or thread-local patterns for logging. Choose StringBuffer when the simplest correct solution for your specific multi-threaded mutable string need is direct method-level synchronisation.
Q5. What is the default capacity of StringBuffer and how does it grow?
The default capacity is 16 characters when created with new StringBuffer(). When the content would exceed the current capacity, StringBuffer allocates a new internal array of size Math.max(minimumNeeded, (currentCapacity + 1) * 2) — roughly doubling the capacity each time. For predictable workloads, setting an appropriate initial capacity with new StringBuffer(n) avoids these resize operations and the associated memory copy overhead.
Q6. What is the output of new StringBuffer("hello").reverse().toString()?
"olleh". reverse() reverses the entire character sequence of the buffer in place and returns this. toString() then creates a String from the reversed buffer. This is a common interview warm-up question — the important concept is that reverse() modifies the buffer itself and returns the same StringBuffer object, which is what enables the chaining with toString().
FAQs
Is StringBuffer still used in modern Java code?
Yes, but rarely compared to StringBuilder. Most string building in modern Java happens in single threads — method-local builders, response construction, query assembly — where StringBuilder is the correct choice. StringBuffer appears in legacy code from Java 1.0 era (before StringBuilder existed), and in genuinely multi-threaded scenarios where a single mutable buffer is shared. When reviewing or maintaining older codebases, you will encounter StringBuffer frequently.
Can you convert between StringBuilder and StringBuffer?
Yes. new StringBuffer(stringBuilder.toString()) creates a StringBuffer from a StringBuilder. new StringBuilder(stringBuffer.toString()) does the reverse. Both constructors accept CharSequence — the interface both implement — so you can also use new StringBuffer(charSequence) and new StringBuilder(charSequence) where charSequence is either type.
Does StringBuffer handle null appends?
Yes. sb.append(null) appends the four characters "null" to the buffer without throwing an exception. This applies to all append() overloads that accept reference types — passing null converts it to the string "null". If you do not want nulls appended as text, add an explicit null check: if (value != null) sb.append(value).
What does the capacity() method return versus length()?
length() returns the current number of characters stored in the buffer — the content size. capacity() returns the size of the internal character array — how much space is allocated. capacity() is always greater than or equal to length(). When length() == capacity(), the next append that increases the content will trigger a resize. ensureCapacity(n) can be called to pre-allocate capacity proactively.
Should you use StringBuffer in new code written today?
Only when you have a demonstrated multi-threaded need for a shared mutable string. For new single-threaded code, use StringBuilder. For assembling strings from collections, use String.join() or streams. For thread-safe string composition where ordering matters, consider synchronized blocks around a StringBuilder. StringBuffer remains valid but should be a deliberate choice based on specific threading requirements, not a default.
Summary
StringBuffer is the thread-safe mutable String type in Java. It solves the problem that String is immutable (creating new objects on every modification) and that StringBuilder is not safe for concurrent access. Every method is synchronised — one thread at a time — making it correct for concurrent writes from multiple threads.
The practical decision is straightforward: use StringBuilder by default for its performance advantage. Use StringBuffer when the same buffer object is demonstrably shared and modified by multiple threads simultaneously. Do not use String concatenation in loops for either case.
For interviews, the core answer is thread safety: StringBuffer synchronises every method, StringBuilder does not. Know that individual method calls are thread-safe but sequences of calls need external synchronisation. Know that StringBuilder was introduced in Java 1.5 specifically to provide an unsynchronised alternative for the common single-threaded case.
What to Read Next
Practice Programs
Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.
Reverse a string using a loop.
Reverses the letters within each word, keeping the word order intact.
Removes every space character from a string.