Skip to content
Java strings 6 min read

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’s toString()) 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 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 StringBuilder an 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:

SpecifierMeaningExample output
%sString"hello"
%dDecimal integer42
%fFloating-point3.140000
%.2f2 decimal places3.14
%nPlatform line break\n on Unix
%-10sLeft-aligned, width 10"hi "

Note: String.format() is convenient but slower than StringBuilder for 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

MethodBest forThread-safePerformance
+ operatorShort, one-off expressionsN/AGood (single expr), poor in loops
StringBuilderLoops, dynamic buildingNoExcellent
StringBufferMulti-threaded buildingYesGood (sync overhead)
String.format()Formatted/padded outputN/AModerate
String.join()Joining fixed list with delimiterN/AGood
StringJoinerBuilding joined strings incrementallyNoGood
Collectors.joining()Stream pipelinesN/AGood
Text blocksMulti-line literal templatesN/AExcellent

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.

Last updated June 13, 2026
Was this helpful?