Java Tutorial
🔍

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

Java
1// File: StringBufferCreation.java 2 3public class StringBufferCreation { 4 5 public static void main(String[] args) { 6 7 // Way 1 — empty buffer (default capacity = 16) 8 StringBuffer sb1 = new StringBuffer(); 9 System.out.println("Empty capacity : " + sb1.capacity()); // 16 10 System.out.println("Empty length : " + sb1.length()); // 0 11 12 // Way 2 — with initial capacity 13 StringBuffer sb2 = new StringBuffer(128); 14 System.out.println("128 capacity : " + sb2.capacity()); // 128 15 16 // Way 3 — with initial content 17 StringBuffer sb3 = new StringBuffer("DevStackFlow"); 18 System.out.println("Content : " + sb3); 19 System.out.println("Content length : " + sb3.length()); // 12 20 System.out.println("Content cap : " + sb3.capacity()); // 16 + 12 = 28 21 22 // Way 4 — from a CharSequence (String, StringBuilder, etc.) 23 CharSequence cs = "Learning Java"; 24 StringBuffer sb4 = new StringBuffer(cs); 25 System.out.println("From CharSeq : " + sb4); 26 27 System.out.println(); 28 29 // Capacity grows automatically when content exceeds it 30 StringBuffer sb5 = new StringBuffer(4); 31 sb5.append("Hello World!"); // 12 chars > 4 capacity 32 System.out.println("Auto capacity : " + sb5.capacity()); // (4+2)*2 = 12 or more 33 System.out.println("Content : " + sb5); 34 } 35}
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()

Java
1// File: StringBufferAppendDemo.java 2 3public class StringBufferAppendDemo { 4 5 public static void main(String[] args) { 6 7 StringBuffer sb = new StringBuffer(); 8 9 // append() — accepts all primitive and reference types 10 sb.append("Order ID : "); 11 sb.append("ORD-2024-001"); 12 sb.append("\n"); 13 sb.append("Amount : Rs."); 14 sb.append(1299.50); // double 15 sb.append("\n"); 16 sb.append("Quantity : "); 17 sb.append(3); // int 18 sb.append("\n"); 19 sb.append("Paid : "); 20 sb.append(true); // boolean 21 22 System.out.println(sb.toString()); 23 System.out.println(); 24 25 // Method chaining — append returns the same StringBuffer 26 StringBuffer header = new StringBuffer() 27 .append("=".repeat(40)) 28 .append("\n") 29 .append(" INVOICE SUMMARY") 30 .append("\n") 31 .append("=".repeat(40)); 32 33 System.out.println(header); 34 } 35}
Output:
Order ID : ORD-2024-001
Amount   : Rs.1299.5
Quantity : 3
Paid     : true

========================================
  INVOICE SUMMARY
========================================

insert(), delete(), replace(), reverse()

Java
1// File: StringBufferModifyDemo.java 2 3public class StringBufferModifyDemo { 4 5 public static void main(String[] args) { 6 7 StringBuffer sb = new StringBuffer("Hello World"); 8 9 // insert(offset, value) — inserts at position 10 sb.insert(5, ","); 11 System.out.println("After insert : " + sb); // Hello, World 12 13 // delete(start, end) — removes a range 14 sb.delete(0, 7); 15 System.out.println("After delete : " + sb); // World 16 17 // deleteCharAt(index) — removes one character 18 sb.deleteCharAt(sb.length() - 1); 19 System.out.println("After delChar : " + sb); // Worl 20 21 // replace(start, end, str) — replaces a range 22 sb.replace(0, 4, "Java"); 23 System.out.println("After replace : " + sb); // Java 24 25 // reverse() — reverses the buffer content 26 sb.reverse(); 27 System.out.println("After reverse : " + sb); // avaJ 28 29 System.out.println(); 30 31 // setCharAt(index, char) — modifies a single character 32 StringBuffer word = new StringBuffer("java"); 33 word.setCharAt(0, 'J'); 34 System.out.println("After setCharAt: " + word); // Java 35 36 // indexOf and lastIndexOf 37 StringBuffer text = new StringBuffer("abcabcabc"); 38 System.out.println("indexOf('abc') : " + text.indexOf("abc")); // 0 39 System.out.println("indexOf('abc', 1) : " + text.indexOf("abc", 1)); // 3 40 System.out.println("lastIndexOf('abc') : " + text.lastIndexOf("abc")); // 6 41 } 42}
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.

Java
1// File: ThreadSafetyDemo.java 2 3public class ThreadSafetyDemo { 4 5 public static void main(String[] args) throws InterruptedException { 6 7 // Test 1 — StringBuilder shared across threads (UNSAFE) 8 StringBuilder unsafeBuilder = new StringBuilder(); 9 10 Runnable appendToSB = () -> { 11 for (int i = 0; i < 1000; i++) { 12 unsafeBuilder.append("A"); // no synchronisation 13 } 14 }; 15 16 Thread t1 = new Thread(appendToSB); 17 Thread t2 = new Thread(appendToSB); 18 t1.start(); t2.start(); 19 t1.join(); t2.join(); 20 21 System.out.println("StringBuilder (unsafe):"); 22 System.out.println(" Expected length: 2000"); 23 System.out.println(" Actual length: " + unsafeBuilder.length()); // may vary! 24 25 System.out.println(); 26 27 // Test 2 — StringBuffer shared across threads (SAFE) 28 StringBuffer safeBuffer = new StringBuffer(); 29 30 Runnable appendToSBF = () -> { 31 for (int i = 0; i < 1000; i++) { 32 safeBuffer.append("A"); // synchronised — safe 33 } 34 }; 35 36 Thread t3 = new Thread(appendToSBF); 37 Thread t4 = new Thread(appendToSBF); 38 t3.start(); t4.start(); 39 t3.join(); t4.join(); 40 41 System.out.println("StringBuffer (thread-safe):"); 42 System.out.println(" Expected length: 2000"); 43 System.out.println(" Actual length: " + safeBuffer.length()); // always 2000 44 } 45}
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

AspectStringStringBuilderStringBuffer
MutableNo — every operation creates new StringYes — modifies in placeYes — modifies in place
Thread-safeYes — immutability makes it inherently safeNo — not synchronisedYes — every method is synchronized
PerformanceSlowest in loops — creates many objectsFastest — no synchronisation overheadSlower than StringBuilder — sync lock overhead
When to useFixed values, constants, comparisonsBuilding strings in a single threadBuilding strings shared across multiple threads
+ operatorYes — compiles to StringBuilder internallyNo — use append()No — use append()
Method return typeNew String — immutable resultthis — same object, chainablethis — same object, chainable
SynchronisedN/A — immutableNoYes — every method
Memory useMany short-lived objects in loopsOne buffer, one final StringOne buffer, one final String
IntroducedJava 1.0Java 1.5Java 1.0
Packagejava.langjava.langjava.lang
Null appendN/AAppends "null"Appends "null"
Capacity defaultN/A1616

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
  │    }                            │
  │  }                              │
  └─────────────────────────────────┘
Java
1// File: WhenToUseDemo.java 2 3import java.util.concurrent.ExecutorService; 4import java.util.concurrent.Executors; 5import java.util.concurrent.TimeUnit; 6 7public class WhenToUseDemo { 8 9 // Single-threaded use — StringBuilder is correct 10 public static String buildReport(String[] items, double[] prices) { 11 StringBuilder sb = new StringBuilder(); // local — not shared 12 sb.append("REPORT\n").append("=".repeat(20)).append("\n"); 13 for (int i = 0; i < items.length; i++) { 14 sb.append(String.format("%-15s Rs.%.2f%n", items[i], prices[i])); 15 } 16 return sb.toString(); 17 } 18 19 // Multi-threaded use — StringBuffer is necessary 20 static class SharedAuditLog { 21 private final StringBuffer log = new StringBuffer(); // shared across threads 22 23 public void write(String threadName, String message) { 24 // StringBuffer ensures no corruption from concurrent writes 25 log.append("[").append(threadName).append("] ") 26 .append(message).append("\n"); 27 } 28 29 public String getLog() { 30 return log.toString(); 31 } 32 } 33 34 public static void main(String[] args) throws InterruptedException { 35 36 // Single-threaded report 37 String[] items = {"Laptop", "Mouse", "Keyboard"}; 38 double[] prices = {45999.0, 599.0, 1299.0}; 39 System.out.println(buildReport(items, prices)); 40 41 // Multi-threaded audit log 42 SharedAuditLog auditLog = new SharedAuditLog(); 43 ExecutorService pool = Executors.newFixedThreadPool(3); 44 String[] threadNames = {"OrderService", "PaymentService", "InventoryService"}; 45 46 for (String name : threadNames) { 47 pool.submit(() -> { 48 for (int i = 1; i <= 3; i++) { 49 auditLog.write(Thread.currentThread().getName(), 50 "Event " + i + " from " + name); 51 try { Thread.sleep(10); } catch (InterruptedException e) { 52 Thread.currentThread().interrupt(); 53 } 54 } 55 }); 56 } 57 58 pool.shutdown(); 59 pool.awaitTermination(5, TimeUnit.SECONDS); 60 61 System.out.println("=== Shared Audit Log (all 9 entries) ==="); 62 System.out.println("Total entries: " + 63 auditLog.getLog().split("\n").length); 64 System.out.println("Log length is non-zero: " + 65 (auditLog.getLog().length() > 0)); 66 } 67}
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.

Real-World Example — Concurrent Request Logger

The Business Problem

A request logging system at a backend service like a PhonePe payment gateway or Razorpay API receives requests from multiple threads simultaneously. Each request handler thread appends its request details to a shared in-memory log buffer. At regular intervals, a separate thread flushes the buffer to disk and clears it. This classic producer-consumer pattern on a shared mutable string requires StringBuffer for safe concurrent writes.

Java
1// File: RequestEntry.java 2 3public class RequestEntry { 4 private final String requestId; 5 private final String endpoint; 6 private final int statusCode; 7 private final long responseTimeMs; 8 private final String threadName; 9 10 public RequestEntry(String requestId, String endpoint, 11 int statusCode, long responseTimeMs) { 12 this.requestId = requestId; 13 this.endpoint = endpoint; 14 this.statusCode = statusCode; 15 this.responseTimeMs = responseTimeMs; 16 this.threadName = Thread.currentThread().getName(); 17 } 18 19 public String format() { 20 return String.format("[%s] %s %s %d %dms%n", 21 threadName, requestId, endpoint, statusCode, responseTimeMs); 22 } 23}
Java
1// File: RequestLogger.java 2 3import java.util.concurrent.atomic.AtomicInteger; 4 5public class RequestLogger { 6 7 // StringBuffer — multiple request handler threads write concurrently 8 private final StringBuffer logBuffer; 9 private final AtomicInteger requestCount; 10 private final int flushThreshold; 11 12 public RequestLogger(int flushThreshold) { 13 this.logBuffer = new StringBuffer(4096); 14 this.requestCount = new AtomicInteger(0); 15 this.flushThreshold = flushThreshold; 16 } 17 18 // Called by multiple threads simultaneously 19 public void log(RequestEntry entry) { 20 logBuffer.append(entry.format()); // synchronised — safe for concurrent threads 21 int count = requestCount.incrementAndGet(); 22 23 if (count % flushThreshold == 0) { 24 flush(); 25 } 26 } 27 28 // Flush current buffer content to console (simulates writing to disk) 29 public synchronized void flush() { 30 if (logBuffer.length() == 0) return; 31 32 System.out.println("=== FLUSH: " + requestCount.get() + " requests logged ==="); 33 System.out.print(logBuffer.toString()); 34 System.out.println("=".repeat(50)); 35 36 // Clear the buffer after flushing 37 logBuffer.delete(0, logBuffer.length()); 38 } 39 40 public int getCount() { return requestCount.get(); } 41 public int getBufferLength() { return logBuffer.length(); } 42}
Java
1// File: RequestHandlerThread.java 2 3import java.util.Random; 4 5public class RequestHandlerThread implements Runnable { 6 7 private final RequestLogger logger; 8 private final String serviceName; 9 private final int requestsToHandle; 10 private final Random random = new Random(); 11 12 private static final String[] ENDPOINTS = { 13 "/api/payment/initiate", 14 "/api/payment/status", 15 "/api/refund/request", 16 "/api/balance/check" 17 }; 18 19 private static final int[] STATUS_CODES = {200, 200, 200, 201, 400, 500}; 20 21 public RequestHandlerThread(RequestLogger logger, 22 String serviceName, 23 int requestsToHandle) { 24 this.logger = logger; 25 this.serviceName = serviceName; 26 this.requestsToHandle = requestsToHandle; 27 } 28 29 @Override 30 public void run() { 31 for (int i = 1; i <= requestsToHandle; i++) { 32 String requestId = serviceName + "-REQ-" + String.format("%03d", i); 33 String endpoint = ENDPOINTS[random.nextInt(ENDPOINTS.length)]; 34 int statusCode = STATUS_CODES[random.nextInt(STATUS_CODES.length)]; 35 long responseTime = 50 + random.nextInt(200); 36 37 RequestEntry entry = new RequestEntry(requestId, endpoint, 38 statusCode, responseTime); 39 logger.log(entry); 40 41 try { 42 Thread.sleep(random.nextInt(20)); 43 } catch (InterruptedException e) { 44 Thread.currentThread().interrupt(); 45 return; 46 } 47 } 48 } 49}
Java
1// File: RequestLoggerDemo.java 2 3import java.util.concurrent.ExecutorService; 4import java.util.concurrent.Executors; 5import java.util.concurrent.TimeUnit; 6 7public class RequestLoggerDemo { 8 9 public static void main(String[] args) throws InterruptedException { 10 11 System.out.println("╔══════════════════════════════════════════╗"); 12 System.out.println("║ CONCURRENT REQUEST LOGGER DEMO ║"); 13 System.out.println("╚══════════════════════════════════════════╝\n"); 14 15 // Logger flushes every 6 requests 16 RequestLogger logger = new RequestLogger(6); 17 18 // 3 handler threads each handling 4 requests = 12 total 19 ExecutorService pool = Executors.newFixedThreadPool(3); 20 pool.submit(new RequestHandlerThread(logger, "payment-svc", 4)); 21 pool.submit(new RequestHandlerThread(logger, "refund-svc", 4)); 22 pool.submit(new RequestHandlerThread(logger, "balance-svc", 4)); 23 24 pool.shutdown(); 25 pool.awaitTermination(10, TimeUnit.SECONDS); 26 27 // Final flush of remaining entries 28 logger.flush(); 29 30 System.out.println("\nTotal requests processed: " + logger.getCount()); 31 System.out.println("Remaining buffer length : " + logger.getBufferLength()); 32 } 33}
Output:
╔══════════════════════════════════════════╗
║    CONCURRENT REQUEST LOGGER DEMO       ║
╚══════════════════════════════════════════╝

=== FLUSH: 6 requests logged ===
[pool-1-thread-1] payment-svc-REQ-001 /api/payment/initiate 200 142ms
[pool-1-thread-2] refund-svc-REQ-001 /api/refund/request 200 98ms
[pool-1-thread-3] balance-svc-REQ-001 /api/balance/check 200 167ms
[pool-1-thread-1] payment-svc-REQ-002 /api/payment/status 201 73ms
[pool-1-thread-2] refund-svc-REQ-002 /api/refund/request 200 55ms
[pool-1-thread-3] balance-svc-REQ-002 /api/balance/check 400 211ms
==================================================
=== FLUSH: 12 requests logged ===
[pool-1-thread-1] payment-svc-REQ-003 /api/payment/initiate 200 88ms
[pool-1-thread-2] refund-svc-REQ-003 /api/refund/request 200 133ms
[pool-1-thread-3] balance-svc-REQ-003 /api/payment/status 500 176ms
[pool-1-thread-1] payment-svc-REQ-004 /api/payment/status 200 62ms
[pool-1-thread-2] refund-svc-REQ-004 /api/balance/check 200 109ms
[pool-1-thread-3] balance-svc-REQ-004 /api/refund/request 200 88ms
==================================================

Total requests processed: 12
Remaining buffer length : 0

Three threads write to the same StringBuffer buffer concurrently. Every log entry is complete and intact — no characters from different threads are interleaved, no entry is lost, and the buffer length is exactly right after each flush. This correctness guarantee is what StringBuffer's synchronisation provides.

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

Java
1// Unnecessary synchronisation overhead 2public String buildUserProfile(String name, String email, int age) { 3 StringBuffer sb = new StringBuffer(); // local variable — never shared between threads 4 sb.append("Name: ").append(name) 5 .append(" | Email: ").append(email) 6 .append(" | Age: ").append(age); 7 return sb.toString(); 8} 9 10// Correct — no threads share this local variable 11public String buildUserProfile(String name, String email, int age) { 12 StringBuilder sb = new StringBuilder(); // no sync overhead needed 13 sb.append("Name: ").append(name) 14 .append(" | Email: ").append(email) 15 .append(" | Age: ").append(age); 16 return sb.toString(); 17}

Mistake 2 — Assuming StringBuffer Makes the Entire Operation Atomic

Java
1// StringBuffer makes each METHOD CALL thread-safe 2// But a SEQUENCE OF CALLS is NOT atomic 3StringBuffer sb = new StringBuffer("Hello"); 4 5// Thread 1 calls: 6int len = sb.length(); // gets 5 — safe 7// Thread 2 appends " World" here — sb is now "Hello World", length 11 8sb.insert(len, "!"); // inserts at position 5 — but sb is now 11 chars long 9// Result might be "Hello! World" or "Hello World!" depending on timing 10 11// If you need a sequence of operations to be atomic, use explicit synchronisation: 12synchronized (sb) { 13 int length = sb.length(); 14 sb.insert(length, "!"); 15}

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

Java
1StringBuffer sb = new StringBuffer(); 2for (String item : items) { 3 sb.append(item).append("\n"); 4 System.out.println(sb.toString()); // creates a new String every iteration — wasteful 5} 6 7// Better — print only at the end, or print the StringBuilder directly in debug mode 8for (String item : items) { 9 sb.append(item).append("\n"); 10} 11System.out.println(sb); // println calls toString() once — acceptable

Mistake 4 — Using StringBuffer for String Constants

Java
1// Wasteful — a simple String constant does not need a mutable buffer 2public static final StringBuffer PREFIX = new StringBuffer("APP_"); 3 4// Correct — constants are immutable by nature, use String 5public 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 StringBufferappend(), 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

TopicLink
How StringBuilder does the same job faster in single-threaded codeJava StringBuilder →
How String immutability explains why mutable alternatives existJava String Basics →
How synchronized keyword works for thread safety in JavaJava Synchronization →
How threads share memory and why synchronisation is necessaryJava Threads →
How String comparison methods work consistently regardless of mutabilityJava String Comparison →
Java StringBuffer | DevStackFlow