What Happens When Concatenating Strings with + Operator
In Java, the + operator is the only overloaded operator available for reference types. When at least one operand of binary + is a String, Java performs a string concatenation op...
🟢 Junior Level
In Java, the + operator is the only overloaded operator available for reference types. When at least one operand of binary + is a String, Java performs a string concatenation operation.
Because String is immutable, the + operator never modifies the existing strings. Instead, it computes and allocates a brand new String object on the Java Heap containing the combined character sequence.
The Essence in 30 Seconds
The behavior of the + operator differs fundamentally depending on whether the operand values are known at compile time:
- Compile-Time Evaluation (Constants): If all operands are string literals or
finalcompile-time constants (e.g.,"A" + "B"), thejavaccompiler merges them ahead of time during compilation (Constant Folding). The compiled.classbytecode contains the pre-merged string"AB", incurring zero concatenation overhead at runtime. - Runtime Evaluation (Dynamic Variables): If any operand is a variable (
str1 + str2), concatenation occurs dynamically at runtime. Starting with Java 9 (JEP 280),javacemits aninvokedynamicbytecode instruction invoking the high-performanceStringConcatFactory(prior to Java 9, the compiler generated aStringBuilderinstance with.append()calls).
// 1. Compile-Time Concatenation (Constant Folding)
String s1 = "Hello " + "World"; // Emitted directly into bytecode constant pool as "Hello World"
// 2. Runtime Concatenation (Evaluated dynamically via invokedynamic)
String name = "Alice";
String greeting = "Hello, " + name + "!";
// 3. Catastrophic Anti-Pattern: Using '+' inside loops (O(N^2) allocations!)
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // AVOID: Allocates 1,000 intermediate String objects on the heap!
}
// 4. Recommended Pattern for Loops:
StringBuilder sb = new StringBuilder(1000 * 4);
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String fastResult = sb.toString();
🟡 Middle Level
Type Conversion Rules & Operator Precedence
Under the Java Language Specification (JLS §15.18.1):
- If either operand of binary
+is of typeString, string concatenation is performed, and the non-string operand is automatically converted to a string. - Primitive types are converted to their standard string representations (
42$\to$"42"). - Reference types are converted via
String.valueOf(obj). If the reference isnull, it converts to the literal string"null". - The
+operator is left-associative (evaluates left-to-right) and shares identical precedence between arithmetic addition and string concatenation.
// Precedence & Associativity Traps:
System.out.println("Sum: " + 10 + 20); // "Sum: 1020" ("Sum: 10" then "Sum: 10" + 20)
System.out.println("Sum: " + (10 + 20)); // "Sum: 30" (Parentheses evaluated first)
System.out.println(10 + 20 + " is sum"); // "30 is sum" (10 + 20 = 30, then 30 + " is sum")
Compile-Time Constant Folding (JLS §15.28)
When operands qualify as Compile-Time Constant Expressions, evaluation occurs entirely during compilation:
final String prefix = "user_";
final int id = 42;
String key = prefix + id; // Folds directly into `ldc "user_42"` in bytecode!
Crucial Constraint: Variables must be declared final and initialized with constant expressions. If a final variable is initialized via a method call (final String a = getPrefix();), the constant folding optimization cannot be applied.
Common Production Concatenation Traps
- Loop Accumulation (
+=):
Each loop iteration creates a new string allocation and copies all previously accumulated characters. Algorithm complexity explodes to $O(N^2)$ in time and memory allocation. - Accidental
nullMasking:
"User ID: " + user.getId()evaluates to"User ID: null"whenuser.getId() == null. This silently masks missing data and can propagate invalid data into databases or downstream APIs. - Eager String Concatenation in Logging:
// BAD: Concatenation executes on every call even when DEBUG level is disabled! logger.debug("Processing order: " + order.getDetails()); // GOOD: Evaluated lazily only if DEBUG is enabled: logger.debug("Processing order: {}", order.getDetails());
🔴 Senior Level
Bytecode Evolution: Java 8 vs. Java 9+ (JEP 280)
Prior to Java 9, javac rigidly desugared concatenation into bytecode that instantiated StringBuilder:
// Original Java source:
String msg = "User: " + name + " age: " + age;
// Desugared bytecode emitted in Java 5 through 8:
new StringBuilder()
.append("User: ")
.append(name)
.append(" age: ")
.append(age)
.toString();
Drawbacks of the Java 8 Model:
- Redundant Resizing: The default constructor
new StringBuilder()allocates 16 characters. If the combined message exceeds 16 characters, 1–2 array reallocations and memory copies occur. - Bytecode Rigidity: The concatenation strategy was hardcoded into compiled
.classfiles. Improvements to runtime concatenation required recompiling all Java bytecode globally.
The Java 9 Breakthrough (JEP 280: Indify String Concatenation):
In Java 9+, javac emits an invokedynamic instruction pointing to StringConcatFactory:
0: aload_1 // Load 'name' variable
1: iload_2 // Load 'age' primitive
2: invokedynamic #7, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;
The .class file includes a Bootstrap Method (BSM) pointing to java.lang.invoke.StringConcatFactory.makeConcatWithConstants.
StringConcatFactory Architecture & Recipe Strings
The bootstrap method accepts a static Recipe String:
- The character
\1designates a dynamic argument passed at runtime. - The character
\2designates a constant extracted from the constant pool.
For "User: " + name + " age: " + age, the recipe evaluates to:
\(\text{Recipe} = \text{"User: \textbackslash 1 age: \textbackslash 1"}\)
Upon the first execution of invokedynamic:
- HotSpot invokes
StringConcatFactory.makeConcatWithConstants. - The factory inspects argument types (
String,int, etc.) and generates an optimalMethodHandle(leveraging internal bytecode generators or direct field access tobyte[] value). - The call site is linked (CallSite Linkage). Subsequent executions invoke compiled machine code directly without reflection or bootstrap overhead.
HotSpot Runtime Concatenation Strategies
The JVM supports multiple runtime strategies (configured via -Djava.lang.invoke.stringConcat=<strategy>):
INLINE_SIZED_EXACT(Default Strategy):- Calculates the exact byte length of the resulting string based on the lengths of all arguments before allocation.
- Allocates a single
byte[]array of the precise required size (honoring Latin-1 or UTF-16 coder flags). - Copies parameter bytes directly into the target array without intermediate buffers.
- Instantiates
Stringdirectly via the package-private zero-copy constructorString(byte[] value, byte coder).
METHOD_HANDLE_INLINE_SIZED_EXACT: Chains method handle combinators.BC_SB(Bytecode StringBuilder): Fallback mode generating traditionalStringBuilderbytecode.
flowchart TD
Invoke[invokedynamic call] --> BSM[StringConcatFactory]
BSM --> ExactLen[Pre-calculate exact length: len = sum of lengths]
ExactLen --> SingleAlloc[Allocate single byte array: byte of len]
SingleAlloc --> DirectCopy[Copy bytes directly into array]
DirectCopy --> ZeroCopyStr[new String without defensive copy]
Why invokedynamic Cannot Optimize Loops
A loop containing for (...) { s += x; } compiles the invokedynamic opcode inside the loop body.
On every iteration, the call site executes anew:
- Calculates the size of the two current operands.
- Allocates a new byte array.
- Allocates a new
Stringobject.
Thus,invokedynamicoptimizes atomic multi-operand expressions on a single line, but cannot optimize loop accumulations.
4 Tricky Interview Questions
1. What does System.out.println("Result: " + 1 + 2 * 3) output, and how does the compiler parse operator precedence?
Answer:
It outputs: "Result: 16".
Analysis: The multiplication operator * has higher precedence than addition/concatenation +.
2 * 3evaluates first to6.- The remaining expression is
"Result: " + 1 + 6. - Operators of equal precedence evaluate strictly left-to-right:
"Result: " + 1concatenates to"Result: 1"."Result: 1" + 6concatenates to"Result: 16".
2. Does concatenation of final variables optimize via Constant Folding if one value is returned by a method?
public class Test {
public static final String A = "Hello";
public static final String B = getSuffix();
public static final String C = A + B;
private static String getSuffix() { return " World"; }
}
Answer:
No, Constant Folding does not apply to variable C.
Under JLS §15.28, a compile-time constant expression can only contain literals, primitive operators, and other compile-time constant variables. A method invocation (getSuffix()) is never a compile-time constant expression, even if the method simply returns a literal.
Because B cannot be evaluated at compile time, A + B is compiled to an invokedynamic call and evaluated at runtime during class initialization (<clinit>).
3. Why does ((String) null) + "abc" execute successfully, while ((String) null).concat("abc") throws NullPointerException?
Answer:
- The
+operator translates under JLS §15.18.1 viaString.valueOf(obj). InsideString.valueOf(), the method explicitly checksreturn (obj == null) ? "null" : obj.toString(). Hence,((String) null) + "abc"converts to"null" + "abc"$\to$"nullabc". - In contrast,
.concat(String str)is an instance method. Calling any method on anullreference results in aninvokevirtualbytecode opcode attempting to dereference a null pointer, immediately throwingNullPointerException. Furthermore,.concat()validates its argument and throwsNPEifstrisnull.
4. Why is concatenating five variables via + in Java 17 faster and consumes less memory than manually writing new StringBuilder().append(...)?
Answer:
Because Java 17 leverages StringConcatFactory (JEP 280):
- Pre-Sized Allocation: The runtime pre-calculates the exact aggregate byte length of all five variables and determines the correct Compact Strings
coder(Latin-1 vs. UTF-16). - Zero Resize Overhead: It allocates exactly one
byte[]array of the required capacity. A manualStringBuilderwith default capacity (16 characters) would trigger multiple array expansions andSystem.arraycopycalls. - No Intermediate Objects: The
StringBuilderobject header (24 bytes) is not allocated on the heap. - Zero-Copy Construction: The package-private constructor
String(byte[] value, byte coder)packages the byte array directly into theStringwithout the defensive copy required byStringBuilder.toString().
🎯 Interview Cheat Sheet
30-Second Summary
“The
+operator in Java produces a new immutableStringobject. For compile-time literals andfinalconstants, the compiler evaluates the result during compilation via Constant Folding, embedding a single string in the constant pool. For dynamic variables, Java 9+ emits aninvokedynamiccall toStringConcatFactory(JEP 280), which pre-calculates the exact required byte array size, eliminates intermediateStringBuilderallocations, and constructs the string faster than manual code. However,+inside loops is strictly prohibited becauseinvokedynamicexecutes per iteration, causing $O(N^2)$ heap allocation churn.”
Key Concepts Checklist
- Constant Folding (JLS §15.28): Literal concatenations resolve at compile time with zero runtime cost.
- JEP 280 (Java 9+):
javacreplacesStringBuilderdesugaring withinvokedynamic. INLINE_SIZED_EXACTStrategy: Pre-calculates exact byte length $\to$ singlebyte[]allocation $\to$ zero-copyStringconstruction.- Null Handling:
+convertsnullto"null"viaString.valueOf(), whereas.concat()throwsNullPointerException. - Loop Accumulation:
+=inside loops is never optimized into a single buffer; always useStringBuilderwith capacity.
Common Pitfalls & Red Flags
- ❌ “In Java 17, the compiler converts string
+intoStringBuilder.” (False: it generatesinvokedynamicpointing toStringConcatFactory). - ❌ “The compiler automatically optimizes
+=inside loops into a single StringBuilder.” (False: each iteration executes a separate allocation, resulting in $O(N^2)$ complexity). - ❌ “A
finalvariable initialized from a method is constant-folded.” (False: method calls are evaluated at runtime, disabling compile-time folding).
Related Topics
- How Java Compiler Optimizes String Concatenation — JEP 280 deep dive
- When to Use StringBuilder vs StringBuffer — Mutable string builders
- Why String is Immutable — Immutability core principles
- What are Compact Strings in Java 9+ —
byte[]memory optimization