☕ Java
StringBuffer
StringBuffer is a mutable sequence of characters that can be modified after creation. Unlike String, which creates a new object for every modification, StringBuffer modifies its internal character buffer in place. StringBuffer was introduced in Java 1.0 and is thread-safe — all its methods are synchronised, making it safe to use from multiple threads. In single-threaded code, StringBuilder (introduced in Java 5) is preferred because it offers the same API without the synchronisation overhead.
Why StringBuffer Exists — The String Immutability Problem
Java strings are immutable — once a String object is created, its character content can never be changed. Operations like concatenation, replacement, and insertion always create new String objects. This immutability provides important benefits: strings can be safely shared between threads, used as HashMap keys without defensive copies, and interned for memory efficiency. But immutability comes with a performance cost when you need to build or modify a string iteratively.
Consider building a comma-separated list by concatenating strings in a loop. Each iteration creates a new String object containing all the previous characters plus the new addition, and the previous string becomes garbage. For n iterations, this creates n String objects and performs O(1 + 2 + 3 + ... + n) = O(n²) character copies. For a list of a thousand items, this is approximately a million character copies — orders of magnitude more work than necessary.
StringBuffer solves this by maintaining a resizable internal character array. Append operations add characters to the end of the buffer without creating intermediate String objects. When the buffer is full, it doubles its capacity — similar to ArrayList's resizing strategy. The total number of character copies for n appends is O(n) amortized, compared to O(n²) for naive string concatenation.
StringBuffer was the original Java solution for mutable string building. It predates Java's thread safety improvements and was designed for use in multi-threaded environments where multiple threads might append to the same buffer. Every public method of StringBuffer is declared synchronized, guaranteeing that only one thread can modify the buffer at a time. This synchronisation is the only substantive difference between StringBuffer and StringBuilder — they have identical APIs and identical performance characteristics, except for the synchronisation overhead of StringBuffer.
Java
// ── The performance problem with String concatenation in a loop: ───────
// BAD — creates many intermediate String objects:
String result = "";
for (int i = 0; i < 1000; i++) {
result += "item" + i + ", "; // creates a new String each iteration
}
// 1000 iterations: ~500,000 character copies (O(n²))
// ── StringBuffer solution — amortised O(n): ───────────────────────────
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 1000; i++) {
sb.append("item").append(i).append(", "); // modifies buffer in place
}
String result2 = sb.toString();
// ── StringBuffer is an object on the heap with an internal char[]: ────
//
// StringBuffer sb:
// capacity = 16 (default initial capacity)
// count = 0 (current number of characters used)
// value = char[16] on heap
//
// After sb.append("Hello"):
// capacity = 16
// count = 5
// value = ['H','e','l','l','o', _, _, ...]
//
// When count reaches capacity, the buffer doubles:
// new capacity = 34 (16*2 + 2)
// System.arraycopy to new char[34]
// ── Thread safety — synchronized methods: ────────────────────────────
StringBuffer sharedBuffer = new StringBuffer();
// Two threads can safely call append() without data corruption:
Thread t1 = new Thread(() -> sharedBuffer.append("Thread1 "));
Thread t2 = new Thread(() -> sharedBuffer.append("Thread2 "));
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(sharedBuffer); // both appends recorded, no corruptionStringBuffer Methods
StringBuffer provides a comprehensive API for string manipulation. The core operations are append(), insert(), delete(), replace(), and reverse(). All mutating methods return the StringBuffer itself, enabling method chaining — a fluent API style where multiple operations are written as a single expression. The capacity() and length() methods distinguish between the allocated buffer size (capacity) and the current content length (characters in use).
The append() method is overloaded for every primitive type and for Object, allowing any value to be appended without explicit conversion. append(char[]) appends an array of characters. append(CharSequence, start, end) appends a substring of a CharSequence. These overloads make StringBuffer the most versatile tool for building strings from mixed data types.
The insert() method inserts content at a specified position, shifting existing content right. The delete() method removes characters from a range. The replace() method replaces a range with a new string. The reverse() method reverses the content. The charAt(), indexOf(), and substring() methods provide read access to the buffer content, matching the String API.
The deleteCharAt() method removes a single character at a specified index — useful for removing a trailing separator character after a loop. The setCharAt() method replaces a single character. The capacity() method returns the current buffer size — typically larger than length() because buffers grow in chunks to amortise reallocation costs.
Java
StringBuffer sb = new StringBuffer("Hello");
// ── append() — add to end: ────────────────────────────────────────────
sb.append(" World");
sb.append('!');
sb.append(42);
sb.append(true);
sb.append(3.14);
System.out.println(sb); // Hello World!42true3.14
// ── Method chaining — all mutating methods return 'this': ─────────────
StringBuffer chained = new StringBuffer()
.append("Name: ")
.append("Alice")
.append(", Age: ")
.append(30)
.append(", Active: ")
.append(true);
System.out.println(chained); // Name: Alice, Age: 30, Active: true
// ── insert() — insert at position: ───────────────────────────────────
StringBuffer sb2 = new StringBuffer("Hello World");
sb2.insert(5, " Beautiful"); // insert at index 5
System.out.println(sb2); // Hello Beautiful World
// ── delete() — remove a range [start, end): ──────────────────────────
StringBuffer sb3 = new StringBuffer("Hello Beautiful World");
sb3.delete(5, 15); // remove indices 5 through 14
System.out.println(sb3); // Hello World
// ── deleteCharAt() — remove single character: ─────────────────────────
StringBuffer sb4 = new StringBuffer("Hellox");
sb4.deleteCharAt(sb4.length() - 1); // remove trailing 'x'
System.out.println(sb4); // Hello
// ── replace() — replace a range with a new string: ────────────────────
StringBuffer sb5 = new StringBuffer("Hello World");
sb5.replace(6, 11, "Java"); // replace "World" with "Java"
System.out.println(sb5); // Hello Java
// ── reverse(): ────────────────────────────────────────────────────────
StringBuffer sb6 = new StringBuffer("abcde");
sb6.reverse();
System.out.println(sb6); // edcba
// ── setCharAt() and charAt(): ─────────────────────────────────────────
StringBuffer sb7 = new StringBuffer("Hello");
sb7.setCharAt(0, 'J');
System.out.println(sb7); // Jello
System.out.println(sb7.charAt(1)); // e
// ── capacity() vs length(): ───────────────────────────────────────────
StringBuffer sb8 = new StringBuffer(); // default capacity 16
System.out.println("Capacity: " + sb8.capacity()); // 16
System.out.println("Length: " + sb8.length()); // 0
sb8.append("Hello");
System.out.println("Capacity: " + sb8.capacity()); // 16 (unchanged)
System.out.println("Length: " + sb8.length()); // 5
// ── toString() — convert to String when done: ─────────────────────────
String finalResult = sb.toString();StringBuffer vs String — Performance Comparison
The performance difference between String concatenation and StringBuffer becomes significant when building strings iteratively. For a small number of concatenations — say, three to five — the difference is negligible and String concatenation is perfectly acceptable. For building strings in a loop with many iterations — creating CSV rows, building SQL queries, generating reports — StringBuffer or StringBuilder is essential.
The Java compiler optimises simple + concatenation of String literals at compile time, folding them into a single String constant. It also optimises + concatenation in non-loop contexts since Java 9 by converting them to invokedynamic calls that use StringBuilder internally. However, the compiler cannot optimise + concatenation inside a loop — it cannot determine at compile time how many iterations will occur, so each iteration creates a new String.
A common performance pattern is to use StringBuffer with a specified initial capacity when you know or can estimate the total size of the result. The default capacity of 16 characters causes many reallocation-and-copy operations for long strings. Specifying new StringBuffer(estimatedLength) avoids these intermediate allocations.
Java
// ── Performance benchmark — String vs StringBuffer: ──────────────────
int iterations = 10_000;
// BAD — String concatenation in loop:
long start1 = System.currentTimeMillis();
String s = "";
for (int i = 0; i < iterations; i++) {
s += "x"; // creates 10,000 intermediate String objects!
}
long time1 = System.currentTimeMillis() - start1;
System.out.println("String concat: " + time1 + "ms"); // ~200ms+
// GOOD — StringBuffer in loop:
long start2 = System.currentTimeMillis();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < iterations; i++) {
sb.append("x"); // one object, no intermediate copies
}
String result = sb.toString();
long time2 = System.currentTimeMillis() - start2;
System.out.println("StringBuffer: " + time2 + "ms"); // ~1ms
// ── Pre-sized capacity avoids reallocation: ───────────────────────────
// Each row is ~50 chars, 1000 rows = ~50,000 chars total
StringBuffer csv = new StringBuffer(50_000); // pre-allocate
String[] names = {"Alice", "Bob", "Charlie"};
int[] scores = {90, 85, 92};
for (int i = 0; i < names.length; i++) {
csv.append(names[i]).append(',').append(scores[i]).append('
');
}
System.out.println(csv);
// ── Compiler optimisations: ───────────────────────────────────────────
// This is optimised at compile time to a single String constant:
String s1 = "Hello" + " " + "World"; // compiled as "Hello World"
// This is optimised by JDK 9+ using StringConcatFactory:
String name = "Alice";
String greeting = "Hello, " + name + "!"; // uses invokedynamic
// But NOT loop concatenation — compiler cannot unroll loops.Related Topics in Strings
String Class
String is one of the most fundamental classes in Java — used in virtually every program, yet deeply misunderstood by many developers. A String represents an immutable sequence of Unicode characters. It is not a primitive type but a full class in java.lang, automatically imported into every Java file. Understanding String means understanding how it is stored in memory, why it is immutable, how the string pool works, what the difference between == and equals() means for strings, and how to use the class efficiently. This entry covers String's nature as a class, its internal representation, the critical distinction between reference equality and value equality, String's place in the type hierarchy, and the design decisions that make String behave the way it does.
String Pool
The string pool (also called the string intern pool or string constant pool) is a special memory region maintained by the JVM that stores a single copy of each unique string value. When two string literals have the same content, they refer to the same object in the pool rather than two separate objects. The pool is a flyweight pattern applied at the language level — it dramatically reduces memory consumption in applications that use many repeated string values, which is nearly every application. This entry covers how the pool works, where it lives in JVM memory, how to interact with it programmatically, the intern() method, performance implications, and when to use or avoid pool entries.
Immutable String
String immutability is the most important design decision in Java's String class. Once a String object is created, its character sequence can never change. No method on String modifies the string; every method that appears to modify returns a new String object containing the result. This design decision drives thread safety, enables the string pool, makes strings safe hash map keys, and simplifies reasoning about string values. Understanding why String is immutable, how immutability is enforced, and what the consequences of immutability are clarifies the behaviour of virtually every piece of Java code that handles strings.
Mutable String
Java provides two mutable string classes for scenarios where String's immutability would be inefficient: StringBuilder and StringBuffer. Both maintain an internal character buffer that can be modified in place — characters can be appended, inserted, deleted, and replaced without creating new objects. StringBuilder is the modern choice for single-threaded use; StringBuffer is the legacy thread-safe version with synchronised methods. This entry covers the internal buffer mechanics, the full API of both classes, performance characteristics, when to use each, thread safety implications, and the complete patterns for efficient string construction.