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,deletemodify 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
JavaString 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
JavaStringBuilder 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
JavaString 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
JavaString 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
JavaString 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
| Type | Mutable | Thread-safe | Typical use |
|---|---|---|---|
| String | No | Safe for read-only | Constants, keys, return values, rare concatenation |
| StringBuilder | Yes | No | Loops, repeated concatenation (single-threaded) |
| StringBuffer | Yes | Yes (synchronized) | Multiple threads sharing one buffer (rare) |
Core Mechanism / Behavior
- String: Internal
char[](orbyte[]+ coder in JDK 9+) isfinal. Any "change" produces a new String. Strings can be interned in the string pool; literals andintern()are stored there. - StringBuilder / StringBuffer: Resizable
char[](orbyte[]).appendgrows the buffer when needed (typically 2×). StringBuffer wraps each method withsynchronized, so only one thread executes at a time. - Initial capacity:
new StringBuilder()defaults to 16 chars. If you know the approximate final length, usenew 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.