RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Java questions
MediumJava#237 min readJul 11, 2026

String vs StringBuilder vs StringBuffer.

practice
learning
Practice modeTest yourself instead of reading straight through

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.”

🧠 Memory Map
Memory map — visual summary of this topic

What they are

  1. String is an immutable object: once it is created, its characters cannot be changed. If you "modify" it, Java creates a new string.
  2. StringBuilder is a mutable sequence of characters. Mutable means the same object can grow, shrink, or be edited without replacing the object.
  3. 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).

How they work under the hood

  1. When you write a simple expression like "a" + "b", the compiler/runtime can optimize it. But that does not make String mutable; it still produces a new result.
  2. For repeated concatenation in a loop, each + can copy the old characters into a new object again and again. That repeated copying is why naive string concatenation can become slow.
  3. 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.
  4. 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.
  5. When you finally call toString(), both builder classes create an immutable String snapshot of the current characters.

Comparison at a glance

TypeMutabilityThread-safeTypical use
StringImmutableYesFixed text
StringBuilderMutableNoSingle-thread building
StringBufferMutableYesShared mutable text

When and why to use each one

  1. Use String when the text does not change: constants, keys, file paths, messages, JSON you already built, and values you share safely across code.
  2. Use StringBuilder when one thread is assembling text step by step: logs, SQL strings, CSV rows, report bodies, or anything inside a loop.
  3. Use 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.
  4. Remember that synchronization protects one method call at a time. If you need a multi-step check-then-act sequence, you still need external synchronization around the whole sequence.

Performance and complexity

  • Repeated 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.
  • If you expect a long result, pre-sizing helps. For example, building a 10 KB message with new StringBuilder(10_240) can avoid multiple resizes and copies.

Important edge cases

  • 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.

Real-world story

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.

Java
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:

  • Why is 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.
  • Why is StringBuilder faster than StringBuffer? It usually avoids synchronization, so it has less locking overhead. In a single thread, that makes repeated appends cheaper.
  • When would you still choose 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.
  • What is the difference between capacity and length? length is how many characters are currently stored; capacity is how much space is reserved before the buffer must grow.
  • Does 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.
  • Tricky: Is 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.
  • Tricky: Does append(null) fail? No, it appends the literal text null. If you want an empty string or placeholder, handle the null before appending.
  • Tricky: Is 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:

  • Using String in a loop. Correction: use StringBuilder for repeated appends so you do not keep copying the same text.
  • Thinking StringBuffer makes all code thread-safe. Correction: only its methods are synchronized; multi-step logic still needs proper locking or a different design.
  • Sharing one StringBuilder across requests. Correction: keep builders local to one thread or one method to avoid corrupted output.
  • Forgetting that 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.
  • Repeated String concatenation in loops can become O(n^2).
  • StringBuilder append is amortized O(1); toString() is O(n).
  • Default builder capacity is 16; pre-size when you know the final length.

Practice Tasks:

  • Rewrite a loop that builds a CSV string using String into one that uses StringBuilder.
  • Change the demo to pre-size the builder for 1,000 characters and observe that the code still works the same.
  • Write a small helper that converts a possibly null value into a safe placeholder before appending it.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

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); } }