String concatenation looks simple, but Java quietly does a lot of work to keep it fast — like a cashier combining many items into one receipt instead of re-writing the whole bill every time.
Question: How is String concatenation optimized?
Answer: Java first tries to fold together anything it can know at compile time, such as string literals and constant expressions. If the parts are only known at runtime, the compiler and JVM use a fast concatenation strategy under the hood, and from Java 9 onward that often goes through invokedynamic and StringConcatFactory instead of always building a visible StringBuilder chain. The big rule is: simple one-off concatenation is usually optimized well, but repeated += inside a loop can still be expensive.
Interview-Ready Answer: I’d say Java optimizes string concatenation in two main ways. First, the compiler folds constant pieces like "a" + "b" into one literal, so there is no runtime work there. Second, for runtime values, older Java versions rewrote concatenation into StringBuilder, while modern Java uses invokedynamic with StringConcatFactory to pick an efficient strategy. The key practical point is that a single expression is usually fine, but repeated concatenation in a loop should use StringBuilder to avoid lots of temporary objects and copying.
When you write "a" + "b", the compiler can prove the result at compile time, so it just stores one string: "ab". This is called constant folding (the compiler pre-computes a value instead of doing the work later). A subtle but important point: final does not automatically mean constant — it is only folded if the value is a compile-time constant expression.
For runtime concatenation, Java used to translate the expression into something like new StringBuilder().append(...).append(...).toString(). Since Java 9, the compiler usually emits invokedynamic, a bytecode instruction that says, “link the right method later.” The JVM then uses StringConcatFactory to choose a good recipe for that exact concatenation shape. That gives the runtime more freedom to pick a strategy that fits the types and number of pieces being joined.
invokedynamic, the first execution links the call site to an efficient concatenation method.String is still immutable. Immutability means the result cannot change after creation, so any builder-style work must happen before the final string is returned.+ and when to switchFor a small number of pieces in one expression, using + is readable and usually fast enough. For building text in a loop, repeated += is the trap: each iteration creates a new string and copies the old content again. That turns a simple-looking program into a copy machine.
| Approach | What Java does | Best for |
|---|---|---|
+ once | Fold or optimize | Small expressions |
StringBuilder | Mutable buffer | Loops, many appends |
StringBuffer | Thread-safe buffer | Rare legacy cases |
StringBuilder is the usual manual tool because it is mutable, meaning it can grow without making a brand-new object for every append. Its default capacity is 16 characters, and it grows as needed, often by roughly doubling. If you know the approximate final size, giving a capacity up front can reduce reallocations.
A single concatenation expression is effectively O(n) in the total number of characters produced, because the final output must be copied somewhere once. The hidden cost is the temporary work: older compilers created builder objects, and modern JVMs still need to assemble the result. In contrast, repeated concatenation in a loop can become O(n^2) overall, because each new step recopies the full string-so-far.
Here is the practical interview answer on performance: if you are appending 1,000 small pieces, the difference becomes noticeable. With +=, the total character copying can explode; with StringBuilder, you usually get one growing buffer and one final toString(). That is why Java developers reach for StringBuilder in log assembly, CSV generation, HTML snippets, and other text-building code.
"x" + null does not throw an exception; it produces "xnull". This surprises people who expect a failure.final variables are only folded if they are true constant expressions.StringBuilder is not synchronized, while StringBuffer is. That makes StringBuilder faster in single-threaded use, which is the common case.Memory hook: think of one string expression as one receipt, but a loop with += as reprinting the whole receipt after every item. Java can streamline the first one; the second one still wastes paper.
Real-World Example: In a checkout service, you might build a human-readable order summary for logs or a receipt email: item names, quantities, and totals. If that text is assembled once per order, Java’s concatenation optimization is usually enough. But if a bug changes the code to append inside a loop with += for every cart item, traffic can create thousands of temporary strings per second.
The outage symptom is often not a crash but slowdowns: higher CPU, more garbage collection, and p99 latency creeping up. Logs may show the service is still healthy, yet users notice delayed receipt emails or checkout confirmation pages loading late. A team might first suspect the database, when the real problem is string building inside a hot loop.
What goes wrong: the code looks innocent, but every loop iteration copies the full accumulated text again. Under load, that means more allocations, more GC pressure, and sudden latency spikes. The fix is simple: switch to StringBuilder, pre-size it if you can estimate the output, and keep the final toString() at the end.
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
public class Main {
public static void main(String[] args) {
// Compile-time folding: the compiler merges literal pieces into one String.
// There is no runtime builder work for this expression.
String folded = "Hello, " + "world" + "!";
System.out.println("Folded literal concat: " + folded);
// Runtime concat: this value is only known when the program runs.
// Java still optimizes this, but it cannot be fully precomputed by the compiler.
String userName = args.length > 0 ? args[0] : null;
String runtime = "User=" + userName;
System.out.println("Runtime concat: " + runtime);
// Edge case: null does not crash concatenation; it becomes the text "null".
// If that is not what you want, make the fallback explicit.
String safeRuntime = "User=" + Objects.toString(userName, "<missing>");
System.out.println("Safe runtime concat: " + safeRuntime);
// In loops, use StringBuilder. Repeated '+' creates a new String each round.
List<String> parts = Arrays.asList("a", "bb", null, "ccc");
System.out.println("Joined with builder: " + joinWithComma(parts));
// This works, but it scales poorly because each iteration copies the growing result.
System.out.println("Joined with '+': " + joinWithPlus(parts));
}
private static String joinWithComma(List<String> parts) {
if (parts == null || parts.isEmpty()) {
return "";
}
// Default StringBuilder capacity is 16 chars, and it grows as needed.
// Pre-sizing is a practical optimization when you know the rough final size.
int estimated = Math.max(16, parts.size() * 8);
StringBuilder sb = new StringBuilder(estimated);
for (int i = 0; i < parts.size(); i++) {
if (i > 0) {
sb.append(", ");
}
// append(null) would add the text "null"; being explicit makes the behavior clear.
sb.append(Objects.toString(parts.get(i), "<null>"));
}
return sb.toString();
}
private static String joinWithPlus(List<String> parts) {
String result = "";
for (int i = 0; i < parts.size(); i++) {
if (i > 0) {
result += ", ";
}
result += Objects.toString(parts.get(i), "<null>");
}
return result;
}
}
Follow-up & Tricky Questions:
+ good enough? For one-off expressions with a few parts, it is fine and readable. Modern Java already optimizes that path well, so you do not need to manually rewrite every small concatenation.+= in a loop bad? Because each iteration creates a new immutable string and copies the old content into it. The total cost grows quickly as the string gets longer.StringBuilder bytecode, Java often uses invokedynamic and StringConcatFactory so the JVM can pick an efficient strategy at runtime.StringBuilder and StringBuffer? StringBuffer is synchronized, so it is thread-safe but usually slower. StringBuilder is the better default for single-threaded text construction.final strings? Only if they are compile-time constants. A final variable that is assigned from a method call is still a runtime value."a" + "b" done at runtime? No. The compiler folds it into one literal, so there is no real concatenation work when the program runs."x" + null throw NullPointerException? No. It produces xnull, because concatenation converts the null reference to the string "null".StringBuilder always beat +? No. For a small one-off expression, Java already does a very good job with +. StringBuilder shines when you build text incrementally, especially in loops.Common Mistakes:
+ is slow. Correction: a single expression is often optimized well; the real danger is repeated concatenation in loops.final always means compile-time constant. Correction: only constant expressions are folded; a final assigned from a method result is still runtime data.StringBuffer by habit. Correction: prefer StringBuilder unless you truly need synchronized access."null"; if that is not acceptable, handle it explicitly with Objects.toString(...) or a conditional.Memory Hook: One receipt is fine; rewriting the whole receipt after every item is waste. That is the difference between one optimized concat expression and a loop with +=.
Cheat Sheet:
invokedynamic and StringConcatFactory for runtime concat.StringBuilder for repeated appends.+= in loops can become O(n^2).StringBuilder is mutable; String is immutable.StringBuffer is synchronized and usually not the default choice.Practice Tasks:
StringBuilder.null values as <missing> instead of null.+= into one that uses a pre-sized StringBuilder.