Why interviewers love this: it quickly reveals whether you understand Java string handling, performance, and thread safety instead of just memorizing class names.
Question: What is the difference between String, StringBuilder, and StringBuffer in Java, and when should you use each one?
Answer: String is immutable, which means every change creates a new object. StringBuilder and StringBuffer are mutable, so they let you change the same object without creating a new one every time. The big difference between the two builders is that StringBuffer is synchronized, meaning it is safe for simple shared use across threads, while StringBuilder is faster but not thread-safe.
Interview-Ready Answer: I use String when the text will not change, StringBuilder when I am doing lots of concatenation in a single thread, and StringBuffer only when I need a mutable string with built-in synchronization. The key performance idea is that repeated + concatenation on String can create many temporary objects, while a builder reuses one buffer.
Detailed Explanation: These three classes all help you work with text, but they solve different problems. String is the standard text type in Java. A StringBuilder is a mutable character sequence, meaning its contents can be changed after creation. StringBuffer is also mutable, but it adds synchronization, which is a mechanism that prevents two threads from changing the same object at the same time.
String stores characters in an internal array and cannot be modified in place. When you do s = s + "x", Java creates a new string with the combined content.StringBuilder keeps a resizable internal character buffer. When it runs out of space, it grows the buffer, usually by allocating a larger array and copying the old content over.StringBuffer uses the same basic idea as StringBuilder, but its methods are synchronized so only one thread at a time can safely mutate it.StringBuffer has extra overhead, so it is usually slower than StringBuilder in single-threaded code.StringBuilder behind the scenes, but that does not change the rule of thumb: in loops, use a builder explicitly when you are repeatedly appending.String for names, IDs, file paths, HTTP headers, log messages, and any value that should not change.StringBuilder when you are assembling text in a loop, building SQL fragments, formatting output manually, or generating JSON-like text in a single thread.StringBuffer only when you truly need a mutable string object shared by multiple threads and you want built-in synchronization.| Feature | String | StringBuilder | StringBuffer |
|---|---|---|---|
| Mutability | No | Yes | Yes |
| Thread safety | Safe by immutability | No | Yes |
| Speed | Good for fixed text | Fastest for edits | Slower than builder |
| Typical use | Read-only text | Single-thread build | Legacy shared text |
Appending one piece at a time with a builder is usually close to O(n) total for the final text size, because the buffer grows occasionally. Repeated String concatenation in a loop can behave much worse because each step copies the whole current text again, which can push it toward O(n^2) work in practice. The default initial capacity of StringBuilder and StringBuffer is typically 16 characters, so tiny strings may reallocate as they grow. That growth strategy is why builders are efficient for assembling larger text pieces.
StringBuilder and StringBuffer are not immutable, so if you pass them around and multiple parts of code mutate them, the final result can be surprising.StringBuffer is thread-safe for individual method calls, but that does not automatically make a whole multi-step sequence logically safe. You may still need external synchronization for a larger operation.toString() on a builder creates a new String. That is usually what you want before returning or storing text.String is perfectly fine. Prematurely using a builder everywhere can make code harder to read without helping performance.Think of String as a sealed letter, StringBuilder as a whiteboard, and StringBuffer as a whiteboard with a lock on the door.
Real-World Story: Imagine a checkout service that builds a long order summary for emails and receipts. If the code concatenates dozens of fields with String inside a loop, every appended line can allocate a new object, which adds GC pressure and slows the request path. A StringBuilder keeps the summary in one growable buffer, so the service stays faster under load.
A misunderstanding here can cause a real outage pattern: CPU rises, latency climbs, and the garbage collector works harder because thousands of short-lived strings are being created every second. In logs you might see normal business events but increasing response times, especially during bulk order exports or notification runs. The fix is often boring but effective: switch repeated concatenation to a builder and reduce temporary object creation.
import java.util.concurrent.CountDownLatch;
public class StringTypesDemo {
public static void main(String[] args) throws InterruptedException {
// 1) String: immutable, so every "change" creates a new object.
String s = "Order";
s = s + " #123";
System.out.println("String result: " + s);
// 2) StringBuilder: best for single-threaded text assembly.
StringBuilder builder = new StringBuilder();
builder.append("Item");
builder.append("-");
builder.append(42);
System.out.println("StringBuilder result: " + builder.toString());
// Edge case: appending null does not throw; it appends the four characters "null".
builder.append("|");
builder.append((String) null);
System.out.println("StringBuilder with null: " + builder);
// 3) StringBuffer: same idea as StringBuilder, but synchronized.
StringBuffer buffer = new StringBuffer();
buffer.append("Safe");
buffer.append("-");
buffer.append("Shared");
System.out.println("StringBuffer result: " + buffer.toString());
// Small threaded demo: each append call is synchronized on StringBuffer.
StringBuffer sharedBuffer = new StringBuffer();
CountDownLatch latch = new CountDownLatch(2);
Runnable task = () -> {
for (int i = 0; i < 3; i++) {
sharedBuffer.append(Thread.currentThread().getName())
.append(":")
.append(i)
.append(" ");
}
latch.countDown();
};
new Thread(task, "T1").start();
new Thread(task, "T2").start();
latch.await();
System.out.println("Shared StringBuffer: " + sharedBuffer.toString().trim());
// Failure path example: forgetting to convert a builder to String before returning.
// Here we show the correct way: toString() gives a true String snapshot.
String finalText = builder.toString();
System.out.println("Snapshot String length: " + finalText.length());
}
}
Follow-up & Tricky Questions:
StringBuilder and StringBuffer? StringBuffer synchronizes its methods, so it is safer for shared use across threads but usually slower.+ concatenation in a loop slow? StringBuffer? StringBuilder ever grow automatically? String always slower than StringBuilder? String is fine and often clearer. Builders pay off when you do many appends.StringBuffer enough to make any code thread-safe? StringBuilder.append(null) throw a NullPointerException? "null", which surprises many candidates.new String("abc") useful? Common Mistakes:
String in a loop for heavy concatenation — fix it by switching to StringBuilder.StringBuffer by default — fix it by using StringBuilder unless you truly need synchronization.toString() before returning text — fix it by converting builders to immutable String at the boundary.Memory Hook: Letter, Whiteboard, Locked Whiteboard — String is a sealed letter, StringBuilder is a writable whiteboard, and StringBuffer is a writable whiteboard with a lock.
Cheat Sheet:
String = immutable text.StringBuilder = fast mutable text, single-thread use.StringBuffer = mutable text with synchronization.StringBuilder for repeated append operations.String for read-only values and simple cases.StringBuffer is mainly for legacy or synchronized shared use.Practice Tasks:
StringBuilder.String concatenation vs StringBuilder in a report generator.StringBuffer or a different synchronization strategy.