String Concatenation
Combining strings is one of the most common operations in Java. The language gives you several ways to do it — from the familiar + operator to StringBuilder, String.format(), and String.join() — and choosing the right tool makes a real difference in readability and performance.
The + Operator
The simplest and most readable approach is the + operator. You can join two or more strings (or a string and a primitive) in a single expression.
String firstName = "Ada";
String lastName = "Lovelace";
String fullName = firstName + " " + lastName;
System.out.println(fullName);
Output:
Ada Lovelace
Non-string primitives are automatically converted to their string representation before joining:
int age = 30;
double gpa = 3.95;
String info = "Age: " + age + ", GPA: " + gpa;
System.out.println(info);
Output:
Age: 30, GPA: 3.95
Note: Java calls
String.valueOf()(or the object’stoString()) automatically when you concatenate a non-String value with+. Null references produce the literal string"null".
Watch Out for Operator Precedence
When you mix + with integers, the compiler evaluates left-to-right, so the order matters:
System.out.println("Sum: " + 1 + 2); // "Sum: 12" — both treated as String concat
System.out.println("Sum: " + (1 + 2)); // "Sum: 3" — arithmetic happens first
Use parentheses when you want arithmetic before concatenation.
Under the Hood: What Does + Compile To?
For a single expression, the Java compiler (since Java 9) translates + concatenation into a call to StringConcatFactory — an invokedynamic instruction that the JVM optimises at runtime. In older Java (≤8), the compiler emitted StringBuilder bytecode directly.
Either way, a simple one-liner like:
String s = a + " " + b;
…is efficient. The problem arises inside loops.
The Loop Trap
// BAD — creates a new String object on every iteration
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // equivalent to: result = new StringBuilder(result).append(i).toString()
}
Each iteration allocates a new StringBuilder, copies the accumulated string, appends, and throws the old objects away. For 1 000 iterations that is 1 000 temporary objects. Prefer StringBuilder here instead.
StringBuilder (Recommended for Loops)
StringBuilder maintains an internal character buffer. You append once, then call toString() at the end — zero unnecessary copies.
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 5; i++) {
sb.append("item").append(i);
if (i < 5) sb.append(", ");
}
System.out.println(sb.toString());
Output:
item1, item2, item3, item4, item5
StringBuilder is not thread-safe. If multiple threads write concurrently, use StringBuffer, which has the same API but synchronises every method.
Tip: Give
StringBuilderan initial capacity hint when you know the rough final length —new StringBuilder(256)— to avoid internal array resizes.
String.format() and Formatted Strings
String.format() (and its instance-method alias .formatted(), added in Java 15) use printf-style format specifiers. They shine when you need precise control over spacing, decimal places, or padding.
String name = "Riya";
int score = 97;
double ratio = 0.974;
String msg = String.format("Player %-10s scored %d (%.1f%%)", name, score, ratio * 100);
System.out.println(msg);
Output:
Player Riya scored 97 (97.4%)
Common format specifiers:
| Specifier | Meaning | Example output |
|---|---|---|
%s | String | "hello" |
%d | Decimal integer | 42 |
%f | Floating-point | 3.140000 |
%.2f | 2 decimal places | 3.14 |
%n | Platform line break | \n on Unix |
%-10s | Left-aligned, width 10 | "hi " |
Note:
String.format()is convenient but slower thanStringBuilderfor tight loops, because it parses the format string at runtime every call. Use it for readability, not bulk assembly.
The .formatted() instance method introduced in Java 15 makes the same call chainable:
// Java 15+
String line = "Hello, %s! You are %d years old.".formatted("Priya", 25);
String.join()
When you need to join a list of values with a delimiter, String.join() (Java 8+) is the cleanest option:
String csv = String.join(", ", "apple", "banana", "cherry");
System.out.println(csv);
Output:
apple, banana, cherry
It also accepts any Iterable:
List<String> langs = List.of("Java", "Python", "Go");
System.out.println(String.join(" | ", langs));
Output:
Java | Python | Go
Internally String.join() uses a StringJoiner, which itself wraps a StringBuilder — so it is efficient.
StringJoiner
StringJoiner (Java 8+) lets you add elements one at a time, and optionally specify a prefix and suffix:
import java.util.StringJoiner;
StringJoiner sj = new StringJoiner(", ", "[", "]");
sj.add("red");
sj.add("green");
sj.add("blue");
System.out.println(sj.toString());
Output:
[red, green, blue]
StringJoiner is especially useful in the Stream API via Collectors.joining().
Collectors.joining() with Streams
import java.util.List;
import java.util.stream.Collectors;
List<String> fruits = List.of("mango", "kiwi", "peach");
String result = fruits.stream()
.map(String::toUpperCase)
.collect(Collectors.joining(", ", "{", "}"));
System.out.println(result);
Output:
{MANGO, KIWI, PEACH}
Text Blocks (Java 15+)
When your “concatenation” is really a multi-line template (JSON, SQL, HTML), a text block is cleaner than string joins:
String json = """
{
"name": "Ada",
"age": 30
}
""";
System.out.println(json);
No \n escapes, no + noise — just clean, readable content.
Quick Comparison
| Method | Best for | Thread-safe | Performance |
|---|---|---|---|
+ operator | Short, one-off expressions | N/A | Good (single expr), poor in loops |
StringBuilder | Loops, dynamic building | No | Excellent |
StringBuffer | Multi-threaded building | Yes | Good (sync overhead) |
String.format() | Formatted/padded output | N/A | Moderate |
String.join() | Joining fixed list with delimiter | N/A | Good |
StringJoiner | Building joined strings incrementally | No | Good |
Collectors.joining() | Stream pipelines | N/A | Good |
| Text blocks | Multi-line literal templates | N/A | Excellent |
Under the Hood
Compile-Time Constant Folding
When both operands are compile-time constants (string literals or final variables), the compiler collapses the expression at compile time — no runtime work at all:
final String A = "foo";
final String B = "bar";
String C = A + B; // compiled as: String C = "foobar";
You can verify this with the javap tool: the constant pool will contain "foobar" directly.
invokedynamic (Java 9+)
From Java 9 onwards, + concatenation uses the invokedynamic bytecode instruction linked to java.lang.invoke.StringConcatFactory. The JVM can pick the optimal strategy (array copy, unsafe access, etc.) at link time — and JIT can re-optimise it later. This is meaningfully faster than the old “always emit StringBuilder” strategy for short, non-looping concatenations.
Memory and the String Pool
Every completed String is a new object on the heap (unless it comes from the String Pool). In a loop that concatenates with +, each iteration produces both a throw-away StringBuilder and a throw-away intermediate String. At scale this stresses the garbage collector — another reason to use StringBuilder in hot paths.
Related Topics
- StringBuilder — the go-to tool for building strings efficiently in loops
- StringBuffer — thread-safe counterpart to StringBuilder
- String Methods — full reference of
StringAPI methods - String Immutability — why String objects can never change, and what that means for concatenation
- String Pool & intern() — how Java reuses string literals to save memory
- Text Blocks — Java 15+ multiline string literals that eliminate concatenation noise