Definition
Three string types:
- String: Immutable.
s += "x"creates a new String. Safe to share but slow for repeated modifications. - StringBuilder: Mutable. Modifies in place without creating new objects. NOT thread-safe. Use for single-threaded building.
- StringBuffer: Mutable. Same API as StringBuilder but all methods are synchronized. Thread-safe but slower.
String is a printed bank statement – any change produces a new printout. StringBuilder is a whiteboard – erase and write freely, fast. StringBuffer is a whiteboard with a lock – only one person writes at a time, slower but safe.
Understanding the Concepts
Comparison table
The three types serve different purposes and have distinct performance characteristics.
| Feature | String | StringBuilder | StringBuffer |
|---|---|---|---|
| Mutable? | No | Yes | Yes |
| Thread-safe? | Yes (immutable) | No | Yes (synchronized) |
| Performance | Slow for loops | Fast | Slower than StringBuilder |
| Since | Java 1.0 | Java 1.5 | Java 1.0 |
| Use when | Simple values, constants | Loop building, single thread | Multi-thread building (rare) |
The loop problem
String concatenation with += in a loop creates a new String object on every iteration. For N iterations, this creates approximately N intermediate objects, generating garbage and causing GC pressure.
package com.bank.report;
import java.math.BigDecimal;
public class StatementBuilder {
// BAD: creates ~10,000 String objects
public String buildBad() {
String result = "";
for (int i = 0; i < 10_000; i++) {
result += "TXN" + i + ": Rs.1000\n"; // new String each iteration
}
return result;
}
// GOOD: one StringBuilder, modifies in place
public String buildGood() {
StringBuilder sb = new StringBuilder(256); // initial capacity
for (int i = 0; i < 10_000; i++) {
sb.append("TXN").append(i).append(": Rs.1000\n");
}
return sb.toString();
}
}
For a banking statement with 10,000 transactions, the StringBuilder version is roughly 100x faster because it modifies a single internal buffer instead of creating thousands of intermediate String objects.
StringBuilder API
StringBuilder provides a rich API for building strings in place.
StringBuilder sb = new StringBuilder("Account: ");
sb.append("SAV2001"); // append text
sb.append(" Balance: ").append(500000); // chaining
sb.insert(0, "[BANK] "); // insert at position
sb.delete(0, 7); // delete range [0, 7)
sb.replace(0, 7, "ACCT"); // replace range
System.out.println(sb.length()); // current length
System.out.println(sb.capacity()); // internal buffer size
String result = sb.toString(); // convert to immutable String
When do you actually need StringBuffer?
Almost never. In modern Java:
- Single-threaded: use StringBuilder.
- Multi-threaded: each thread builds its own StringBuilder (no sharing needed).
- StringBuffer is legacy. Use it only when maintaining old code that explicitly shares a buffer across threads.
package com.bank.service;
import java.util.List;
import java.util.stream.Collectors;
public class TransactionFormatter {
// Modern approach: no shared mutable state needed
public String formatTransactions(List<String> txnIds) {
return txnIds.stream()
.map(id -> "TXN: " + id)
.collect(Collectors.joining("\n"));
}
}
What about Java 9+ String concatenation optimization?
Since Java 9, the compiler uses invokedynamic and StringConcatFactory for + concatenation, which is often faster than manual StringBuilder for simple expressions.
// Java 9+: compiled with invokedynamic, efficient
String info = "Account: " + accountId + " Balance: " + balance;
// This is fine – no need for StringBuilder here
// But in LOOPS, StringBuilder is still necessary:
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // Still bad! The compiler does NOT optimize += in loops
}
The key rule: simple concatenation outside loops is fine with +. Inside loops, always use StringBuilder.
What about String.join() and Collectors.joining()?
For joining collections, Java provides cleaner alternatives to manual StringBuilder.
// String.join() – joining with a delimiter
List<String> accounts = List.of("SAV2001", "CUR3001", "LOAN5001");
String joined = String.join(", ", accounts); // "SAV2001, CUR3001, LOAN5001"
// Collectors.joining() – in a stream pipeline
String csv = accounts.stream()
.collect(Collectors.joining(",", "[", "]")); // "[SAV2001,CUR3001,LOAN5001]"
What should a senior engineer know?
- StringBuilder initial capacity:
new StringBuilder(256)avoids resizing. Default is 16 characters. When the buffer fills, it doubles in size and copies, so pre-sizing for the expected output length saves allocations. - StringBuilder is not thread-safe. If you need a shared builder (rare), use StringBuffer or synchronize externally.
- In banking at National Bank of India, Rajesh uses StringBuilder for generating bank statements and transaction reports in batch jobs. For simple log messages,
+concatenation is fine since Java 9.
Visual Overview
String: immutable, new object per change
StringBuilder: mutable, not synchronized, fast
StringBuffer: mutable, synchronized, thread-safe, slow
Loop:
String += → N new objects → slow, GC pressure
StringBuilder.append → 1 object, in-place → fast
Rule: StringBuilder by default
StringBuffer only for legacy multi-thread code
String for simple values and constants
Key Takeaway
String is immutable – every modification creates a new object. StringBuilder is mutable and fast for single-threaded building. StringBuffer is mutable and thread-safe but slower. Use StringBuilder for loops and concatenation. Use StringBuffer only in legacy code that shares a builder across threads.
Since Java 9, simple + concatenation is optimized by the compiler. But in loops, StringBuilder is still essential.
One sentence to remember: String is immutable, StringBuilder is fast and mutable, StringBuffer adds synchronization – use StringBuilder by default.
---