String, StringBuilder, StringBuffer

String is immutable; StringBuilder is mutable and intended for single-threaded use; StringBuffer is mutable with synchronized methods for thread safety. In most cases, prefer StringBuilder for concatenation and use a new instance per thread rather than sharing a StringBuffer. This article compares the three, explains performance implications, and gives practical guidance with examples and tables.

Overview

  • String: Immutable. Every "modification" creates a new object. Heavy concatenation in a loop produces many temporary objects and copies, leading to O(n²) behavior and GC pressure. Use for constants, keys, return values, or when concatenation is rare.
  • StringBuilder: Mutable character sequence. append, insert, delete modify the buffer in place. No synchronization. Preferred for loops and repeated concatenation in single-threaded code.
  • StringBuffer: Same API as StringBuilder, but all public methods are synchronized. Thread-safe for concurrent modification of a single instance. Rarely needed; usually "one StringBuilder per thread" and then combine results is simpler and faster.
  • Choice: For loops or repeated concatenation → StringBuilder. For read-only, constants, or rare concatenation → String. For multiple threads sharing one buffer → StringBuffer (uncommon); more common is one StringBuilder per thread or immutable String.

Example

Example 1: Avoid — String concatenation in a loop

Java
String s = "";
for (int i = 0; i < 10000; i++) {
    s = s + "," + i;  // Each iteration: new String, copy old content — O(n²) and many allocations
}

Each s + "," + i creates a new String and copies the previous content. Total time is roughly O(n²). GC pressure is high. Do not do this for large loops.

Example 2: Prefer — StringBuilder

Java
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    if (sb.length() > 0) sb.append(',');
    sb.append(i);
}
String s = sb.toString();

Single allocation for the buffer; append is amortized O(1). Total time O(n). One final toString() copies the content to a new String.

Example 3: Compile-time constant folding

Java
String s = "hello" + " " + "world";  // Compiler folds to "hello world" — one literal

When all operands are compile-time constants, the compiler concatenates them into a single literal. No runtime cost.

Example 4: Runtime concatenation — compiler uses StringBuilder

Java
String a = getA();
String b = getB();
String c = a + b;  // Compiler generates: new StringBuilder().append(a).append(b).toString()

For a simple a + b, the compiler uses StringBuilder internally. In a loop, however, it may create a new StringBuilder per iteration, so explicit StringBuilder is better.

Example 5: Multi-threaded — one StringBuilder per thread

Java
String result = IntStream.range(0, 10)
    .parallel()
    .mapToObj(i -> {
        StringBuilder sb = new StringBuilder();
        sb.append("part-").append(i);
        return sb.toString();
    })
    .collect(Collectors.joining(", "));

Each thread has its own StringBuilder; no shared mutable state. No need for StringBuffer.

Example 6: When StringBuffer might be used

Java
// Shared buffer updated by multiple threads (rare)
StringBuffer sb = new StringBuffer();
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i = 0; i < 4; i++) {
    pool.submit(() -> sb.append(Thread.currentThread().getName()).append("\n"));
}
pool.shutdown();
pool.awaitTermination(5, TimeUnit.SECONDS);
// sb contains lines from all threads — but usually you'd use one StringBuilder per task and merge

If you truly need multiple threads appending to the same buffer, StringBuffer is thread-safe. In practice, per-thread StringBuilder plus merging is usually clearer and faster.

Comparison Table

TypeMutableThread-safeTypical use
StringNoSafe for read-onlyConstants, keys, return values, rare concatenation
StringBuilderYesNoLoops, repeated concatenation (single-threaded)
StringBufferYesYes (synchronized)Multiple threads sharing one buffer (rare)

Core Mechanism / Behavior

  • String: Internal char[] (or byte[] + coder in JDK 9+) is final. Any "change" produces a new String. Strings can be interned in the string pool; literals and intern() are stored there.
  • StringBuilder / StringBuffer: Resizable char[] (or byte[]). append grows the buffer when needed (typically 2×). StringBuffer wraps each method with synchronized, so only one thread executes at a time.
  • Initial capacity: new StringBuilder() defaults to 16 chars. If you know the approximate final length, use new StringBuilder(estimatedCapacity) to avoid resizing.

Key Rules

  • Loops or repeated concatenation: Use StringBuilder (single-threaded), then toString() to get the final String.
  • Read-only, shared, or rare concatenation: Use String. For thread safety, prefer one StringBuilder per thread or immutable String; avoid StringBuffer unless you truly need a shared mutable buffer.
  • Initial capacity: If you know the approximate size, use new StringBuilder(capacity) to reduce allocations.
  • After building, use String for storage or as a key; it is immutable and safe to share.

What's Next

See Java Collections for collection usage with strings. See JVM articles for string pooling and GC behavior.