String Concatenation
Concatenation means joining strings end to end. Java offers several ways to do it: the + operator, the concat method, String.join, StringJoiner, Collectors.joining, StringBuilder, and formatting methods. They produce the same kind of result but differ in readability, how they treat non-string values and null, and performance when text is built up repeatedly. Choosing the right one is a small decision that comes up constantly.
The + Operator
When at least one operand of + is a String, Java performs concatenation instead of addition:
String first = "Ada";
String last = "Lovelace";
String full = first + " " + last; // "Ada Lovelace"
int year = 1815;
String info = full + " was born in " + year; // "Ada Lovelace was born in 1815"
The non-string operand is converted to text automatically: numbers become their decimal form, chars become one-character strings, booleans become "true"/"false", and objects are converted by calling their toString() method.
Evaluation order matters
+ is evaluated left to right. Once the running result is a String, every following + is concatenation:
System.out.println(1 + 2 + "3" + 4 + 5); // 3345
Step by step: 1 + 2 is integer addition (3); 3 + "3" is concatenation ("33"); then "33" + 4 gives "334", and + 5 gives "3345". Use parentheses to force arithmetic:
System.out.println("Sum: " + 4 + 5); // Sum: 45
System.out.println("Sum: " + (4 + 5)); // Sum: 9
System.out.println("Total: " + 2 * 3); // Total: 6 (* binds tighter than +)
char is a number unless a String is involved
System.out.println("" + 'a' + 'b'); // ab
System.out.println('a' + 'b'); // 195: 97 + 98, integer addition
Two char values added together produce an int. Starting with a string, or using String.valueOf(c), forces concatenation.
null becomes "null"
String name = null;
System.out.println("Name: " + name); // Name: null
Concatenation never throws for null; it inserts the four characters null. That is convenient for debug logging but almost never what a user should see, so check values or supply defaults (Objects.requireNonNullElse(name, "guest")) before building user-facing text.
Compound assignment
String path = "home";
path += "/docs";
path += "/java"; // "home/docs/java"
a += b is shorthand for a = a + b. It creates a new string each time; the original string object is unchanged.
The concat Method
String s = "Java".concat(" ").concat("21"); // "Java 21"
concat accepts only a String argument (no automatic conversion of numbers) and throws NullPointerException if the argument is null. It is rarely preferred over +.
How + Is Compiled
For a single expression such as a + " " + b + "!", the compiler generates efficient code. Since Java 9, it uses an invokedynamic call to a JVM-provided concatenation strategy that sizes the result once and copies each part directly. A single concatenation expression is therefore fast and needs no manual optimization.
Concatenation in Loops: Use StringBuilder
The problem appears when a string is built up across loop iterations:
String result = "";
for (int i = 0; i < 10_000; i++) {
result += i + ","; // creates a new, longer string every time
}
Each += copies everything built so far into a brand-new string. The total work grows with the square of the output length: for 10,000 items, the program copies tens of millions of characters and creates 10,000 discarded strings.
StringBuilder keeps a single growable buffer and appends into it:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10_000; i++) {
sb.append(i).append(',');
}
String result = sb.toString();
The work now grows linearly with the output size. append is overloaded for every primitive type and for objects, and returns the builder itself, so calls can be chained. StringBuilder has its own lesson.
| Situation | Use |
|---|---|
| One expression joining a few values | + |
| Appending inside a loop | StringBuilder |
| Joining items with a separator | String.join, StringJoiner, Collectors.joining |
| Mixing text with formatted numbers | String.format / formatted |
Joining with a Separator
Putting a separator between items (but not after the last one) is so common that Java has dedicated tools.
String.join
String csv = String.join(",", "red", "green", "blue"); // "red,green,blue"
String path = String.join("/", List.of("home", "ada", "docs")); // "home/ada/docs"
It accepts varargs or any Iterable of CharSequences.
StringJoiner
StringJoiner adds items one at a time, with an optional prefix and suffix:
StringJoiner sj = new StringJoiner(", ", "[", "]");
sj.add("Java").add("Bash").add("C");
System.out.println(sj); // [Java, Bash, C]
StringJoiner empty = new StringJoiner(", ", "[", "]");
empty.setEmptyValue("none");
System.out.println(empty); // none
It is useful when items are added conditionally inside a loop.
Collectors.joining
With streams, Collectors.joining does the same job:
String names = students.stream()
.map(Student::name)
.collect(Collectors.joining(", "));
String bracketed = Stream.of("a", "b").collect(Collectors.joining(" | ", "[", "]")); // [a | b]
All three avoid the classic loop bug of a trailing separator ("a, b, c, ").
Formatting Instead of Concatenating
When text mixes several values with formatting needs, a format string is often clearer than a chain of +:
String msg1 = "Order " + id + " for " + name + ": " + qty + " x " + price;
String msg2 = String.format("Order %d for %s: %d x %.2f", id, name, qty, price);
String msg3 = "Order %d for %s: %d x %.2f".formatted(id, name, qty, price);
The format version shows the shape of the output at a glance and controls number formatting. The String Format lesson covers format specifiers.
Practical Example: Receipt and CSV Export
import java.util.*;
import java.util.stream.Collectors;
public class Concat {
record Line(String item, int qty, double price) { }
static String csvRow(List<String> fields) {
StringJoiner row = new StringJoiner(",");
for (String f : fields) {
row.add(f.contains(",") || f.contains("\"")
? "\"" + f.replace("\"", "\"\"") + "\""
: f);
}
return row.toString();
}
public static void main(String[] args) {
List<Line> lines = List.of(
new Line("Notebook", 3, 80), new Line("Pen, blue", 10, 12.5), new Line("Bag \"XL\"", 1, 1499));
StringBuilder receipt = new StringBuilder();
double total = 0;
for (Line l : lines) {
double amount = l.qty() * l.price();
total += amount;
receipt.append(String.format("%-12s %3d x %8.2f%n", l.item(), l.qty(), l.price()));
}
receipt.append("-".repeat(28)).append('\n')
.append(String.format("%-12s %15.2f", "TOTAL", total));
System.out.println(receipt);
System.out.println(csvRow(List.of("item", "qty", "price")));
for (Line l : lines) {
System.out.println(csvRow(List.of(l.item(), String.valueOf(l.qty()), String.valueOf(l.price()))));
}
String tags = lines.stream().map(Line::item)
.collect(Collectors.joining(" | ", "[", "]"));
System.out.println(tags);
}
}
Output:
Notebook 3 x 80.00
Pen, blue 10 x 12.50
Bag "XL" 1 x 1499.00
----------------------------
TOTAL 1864.00
item,qty,price
Notebook,3,80.0
"Pen, blue",10,12.5
"Bag ""XL""",1,1499.0
[Notebook | Pen, blue | Bag "XL"]
Each technique has a role:
StringBuilderfor the receipt. Lines are appended inside a loop, and each line is produced byString.formatso the columns align.repeat(28)for the divider. It builds the rule of hyphens without a loop.StringJoinerfor CSV fields. It puts commas only between fields. Fields that contain a comma or a quote are wrapped in quotes, and embedded quotes are doubled, following the CSV convention, so thatPen, bluestays a single field when the file is opened in a spreadsheet.Collectors.joiningfor the tag line. It joins the item names with a separator and wraps them in brackets.
None of the output was built with += in a loop.