Hook: Interviewers love this question because one word — immutable vs mutable — affects performance, threading, and bug risk all at once.
Question: What is the difference between String, StringBuilder, and StringBuffer in Java?
Answer: String is immutable, which means its value cannot change after it is created. StringBuilder and StringBuffer are mutable, so they can be changed in place; StringBuilder is usually faster, while StringBuffer is synchronized, meaning its methods are protected for use by multiple threads.
Interview-Ready Answer: “I use String for fixed text because it is immutable and easy to share safely. For repeated appends or building a long message in a loop, I prefer StringBuilder because it avoids creating lots of temporary objects. If the same text object must be mutated by multiple threads, I use StringBuffer, which synchronizes its methods — although in modern code I usually prefer StringBuilder with better design or external locking when shared mutation is required.”
String is an immutable object: once it is created, its characters cannot be changed. If you "modify" it, Java creates a new string.StringBuilder is a mutable sequence of characters. Mutable means the same object can grow, shrink, or be edited without replacing the object.StringBuffer is also mutable, but its methods are synchronized (protected by a lock so only one thread can run them at a time on the same object)."a" + "b", the compiler/runtime can optimize it. But that does not make String mutable; it still produces a new result.+ can copy the old characters into a new object again and again. That repeated copying is why naive string concatenation can become slow.StringBuilder keeps an internal resizable buffer. In the JDK, the default capacity is 16 characters, and it grows when needed. If you know the final size, you can pre-size it to reduce copying.StringBuffer uses the same basic idea as StringBuilder, but every public mutating method is synchronized. That gives method-level thread safety, but it also adds lock overhead.toString(), both builder classes create an immutable String snapshot of the current characters.| Type | Mutability | Thread-safe | Typical use |
|---|---|---|---|
| String | Immutable | Yes | Fixed text |
| StringBuilder | Mutable | No | Single-thread building |
| StringBuffer | Mutable | Yes | Shared mutable text |
String when the text does not change: constants, keys, file paths, messages, JSON you already built, and values you share safely across code.StringBuilder when one thread is assembling text step by step: logs, SQL strings, CSV rows, report bodies, or anything inside a loop.StringBuffer only when the same mutable text object is truly shared across threads and you want built-in synchronization. In many modern designs, a better choice is to avoid sharing the buffer at all.String concatenation in a loop is often O(n^2) overall, because each new result copies the previous characters again.StringBuilder.append() is usually amortized O(1) per append, with toString() being O(n) because it must produce a final immutable string.StringBuffer has the same big-O shape as StringBuilder, but it usually has extra overhead from locking.new StringBuilder(10_240) can avoid multiple resizes and copies.StringBuilder.append(null) does not throw an exception; it appends the text null. That surprises many candidates.setLength(0) clears a builder, but it does not necessarily shrink its capacity. That is useful for reuse, but a very large builder can keep memory reserved.StringBuilder is safe only when confined to one thread. Putting it in a shared field and mutating it from multiple requests can corrupt output.String literals may be reused from the string pool, which is a JVM cache for string constants. That helps sharing, but it does not change immutability.Memory model: Think of String as a carved stone tablet, StringBuilder as a whiteboard, and StringBuffer as a whiteboard with a lock on the door.
A checkout service builds order confirmation emails. Each email has the customer name, line items, tax, and total. The team uses StringBuilder inside the request handler, because one request is handled by one thread and the text is assembled piece by piece.
What goes wrong when people misunderstand this: a developer stores one StringBuilder in a shared field and reuses it for every request. Under load, two requests append to the same buffer at the same time, so one customer may receive another customer’s item list mixed into their receipt. In logs, you may see garbled lines, strange duplicates, or emails that end mid-sentence. Users report wrong totals or broken confirmations, and the bug appears only during traffic spikes because it is a concurrency problem.
Why this matters: the right class is not just about speed; it is about ownership. If the text belongs to one thread, use StringBuilder. If it must be shared, redesign first, then use synchronization only if truly needed.
import java.util.Arrays;
import java.util.List;
public class StringVsStringBuilderVsStringBufferDemo {
public static void main(String[] args) {
demonstrateStringImmutability();
demonstrateStringBuilder();
demonstrateStringBuffer();
demonstrateNullAppendEdgeCase();
}
private static void demonstrateStringImmutability() {
System.out.println("=== String: immutable ===");
String original = "Hello";
String changed = original.concat(" world");
// The original string is unchanged because String objects cannot be edited in place.
System.out.println("original = " + original);
System.out.println("changed = " + changed);
System.out.println("same object? " + (original == changed));
// Naive concatenation in a loop creates many temporary String objects.
List<String> items = Arrays.asList("Keyboard", "Mouse", "USB-C Cable");
System.out.println(buildWithString(items));
}
private static String buildWithString(List<String> items) {
String result = "Receipt:";
for (String item : items) {
// Each += can allocate a fresh String and copy the old characters again.
result += "\n- " + item;
}
return result;
}
private static void demonstrateStringBuilder() {
System.out.println("\n=== StringBuilder: mutable, fast in one thread ===");
StringBuilder sb = new StringBuilder();
System.out.println("default capacity = " + sb.capacity());
sb.append("Order #").append(42).append(": ");
sb.append("packed");
System.out.println(sb.toString());
// Reuse is common: clear the content but keep the buffer for the next message.
sb.setLength(0);
sb.append("Reused buffer, length = ").append(sb.length())
.append(", capacity = ").append(sb.capacity());
System.out.println(sb.toString());
}
private static void demonstrateStringBuffer() {
System.out.println("\n=== StringBuffer: mutable + synchronized methods ===");
StringBuffer buffer = new StringBuffer();
buffer.append("Thread-safe ").append("append sequence");
System.out.println(buffer.toString());
// Same API as StringBuilder, but each mutating method is synchronized.
// That adds overhead, so prefer StringBuilder unless shared mutable access is required.
buffer.append(" | length = ").append(buffer.length());
System.out.println(buffer.toString());
}
private static void demonstrateNullAppendEdgeCase() {
System.out.println("\n=== Edge case: appending null ===");
String maybeNull = null;
StringBuilder builder = new StringBuilder("Customer: ");
// This does not throw. It appends the four characters 'null'.
builder.append(maybeNull);
System.out.println(builder.toString());
// If 'null' should mean missing data, handle it explicitly.
String safe = maybeNull == null ? "<missing>" : maybeNull;
System.out.println("Customer: " + safe);
}
}
Follow-up & Tricky Questions:
String immutable? Because immutability makes sharing safe, supports string pooling, and prevents accidental changes to values used as keys, cache entries, or security-sensitive data.StringBuilder faster than StringBuffer? It usually avoids synchronization, so it has less locking overhead. In a single thread, that makes repeated appends cheaper.StringBuffer? When legacy code or a shared mutable text object really needs built-in method synchronization. In new code, many teams prefer to avoid shared mutation instead.capacity and length? length is how many characters are currently stored; capacity is how much space is reserved before the buffer must grow.a + b + c always create multiple objects? Not necessarily in a single expression, because the compiler/runtime can optimize it. But repeated concatenation in a loop is still a common source of extra allocations.StringBuffer completely thread-safe? Only its individual methods are synchronized. If your logic spans multiple calls, you may still need external locking to keep the whole sequence atomic.append(null) fail? No, it appends the literal text null. If you want an empty string or placeholder, handle the null before appending.StringBuilder safe if it is stored in a local variable? It is safe only because the local variable is usually confined to one thread. If you pass that builder to multiple threads, it becomes unsafe again.Common Mistakes:
String in a loop. Correction: use StringBuilder for repeated appends so you do not keep copying the same text.StringBuffer makes all code thread-safe. Correction: only its methods are synchronized; multi-step logic still needs proper locking or a different design.StringBuilder across requests. Correction: keep builders local to one thread or one method to avoid corrupted output.append(null) writes null. Correction: guard null values explicitly if that text is not desired.Memory Hook: String is a stone tablet, StringBuilder is a whiteboard, and StringBuffer is a whiteboard with a lock.
Cheat Sheet:
String = immutable, safe to share, best for fixed text.StringBuilder = mutable, fastest for one-thread text construction.StringBuffer = mutable + synchronized, older and slower, used when shared mutation is required.String concatenation in loops can become O(n^2).StringBuilder append is amortized O(1); toString() is O(n).Practice Tasks:
String into one that uses StringBuilder.null value into a safe placeholder before appending it.