StringJoiner is a utility class introduced in Java 8 that builds a sequence of characters separated by a delimiter, with optional prefix and suffix. It lives in java.util and solves a problem every Java developer has faced: how do you join a list of strings with commas (or any delimiter) without an ugly trailing comma at the end?
Before Java 8, building a delimited string required manual StringBuilder logic — appending elements, tracking whether to add a delimiter, and trimming the last character. StringJoiner handles all of this automatically.
import java.util.List;
import java.util.StringJoiner;
public class StringJoinerIntro {
public static void main(String[] args) {
List fruits = List.of("Apple", "Banana", "Cherry");
// BEFORE Java 8: manual StringBuilder with trailing delimiter issue
StringBuilder sb = new StringBuilder();
for (int i = 0; i < fruits.size(); i++) {
sb.append(fruits.get(i));
if (i < fruits.size() - 1) {
sb.append(", ");
}
}
System.out.println(sb.toString());
// Output: Apple, Banana, Cherry
// WITH StringJoiner: clean and simple
StringJoiner joiner = new StringJoiner(", ");
for (String fruit : fruits) {
joiner.add(fruit);
}
System.out.println(joiner.toString());
// Output: Apple, Banana, Cherry
}
}
The simplest StringJoiner constructor takes just a delimiter. You add elements with add() and get the result with toString().
The StringJoiner(CharSequence delimiter) constructor creates a joiner with the given delimiter between elements. The add() method appends a new element. Elements are added in order, and the delimiter is automatically inserted between them.
import java.util.StringJoiner;
public class BasicStringJoiner {
public static void main(String[] args) {
// Comma delimiter
StringJoiner csv = new StringJoiner(", ");
csv.add("Alice");
csv.add("Bob");
csv.add("Charlie");
System.out.println(csv.toString());
// Output: Alice, Bob, Charlie
// Pipe delimiter
StringJoiner piped = new StringJoiner(" | ");
piped.add("Red");
piped.add("Green");
piped.add("Blue");
System.out.println(piped.toString());
// Output: Red | Green | Blue
// Newline delimiter
StringJoiner lines = new StringJoiner("\n");
lines.add("Line 1");
lines.add("Line 2");
lines.add("Line 3");
System.out.println(lines.toString());
// Output:
// Line 1
// Line 2
// Line 3
// Single element: no delimiter added
StringJoiner single = new StringJoiner(", ");
single.add("Only One");
System.out.println(single.toString());
// Output: Only One
// Empty joiner: returns empty string
StringJoiner empty = new StringJoiner(", ");
System.out.println("Empty: '" + empty.toString() + "'");
// Output: Empty: ''
}
}
By default, an empty StringJoiner (one with no elements added) returns an empty string for delimiter-only constructors, or just the prefix + suffix for the three-argument constructor. You can override this with setEmptyValue() to return a custom string when no elements are added.
import java.util.StringJoiner;
public class SetEmptyValueDemo {
public static void main(String[] args) {
// Without setEmptyValue: returns prefix + suffix (or empty string)
StringJoiner joiner1 = new StringJoiner(", ", "[", "]");
System.out.println("Default empty: " + joiner1.toString());
// Output: Default empty: []
// With setEmptyValue: returns custom value when empty
StringJoiner joiner2 = new StringJoiner(", ", "[", "]");
joiner2.setEmptyValue("No items found");
System.out.println("Custom empty: " + joiner2.toString());
// Output: Custom empty: No items found
// Once an element is added, setEmptyValue is ignored
joiner2.add("Apple");
System.out.println("After add: " + joiner2.toString());
// Output: After add: [Apple]
// Useful for displaying "N/A" or "none" in reports
StringJoiner tags = new StringJoiner(", ");
tags.setEmptyValue("(no tags)");
System.out.println("Tags: " + tags.toString());
// Output: Tags: (no tags)
}
}
The three-argument constructor StringJoiner(delimiter, prefix, suffix) adds a prefix before the first element and a suffix after the last element. This is extremely useful for building structured output like JSON arrays, SQL IN clauses, and HTML lists.
import java.util.StringJoiner;
public class PrefixSuffixDemo {
public static void main(String[] args) {
// JSON-like array output
StringJoiner json = new StringJoiner(", ", "[", "]");
json.add("\"Apple\"");
json.add("\"Banana\"");
json.add("\"Cherry\"");
System.out.println(json.toString());
// Output: ["Apple", "Banana", "Cherry"]
// SQL IN clause
StringJoiner sql = new StringJoiner(", ", "WHERE status IN (", ")");
sql.add("'ACTIVE'");
sql.add("'PENDING'");
sql.add("'APPROVED'");
System.out.println(sql.toString());
// Output: WHERE status IN ('ACTIVE', 'PENDING', 'APPROVED')
// HTML unordered list
StringJoiner html = new StringJoiner("\n The merge() method combines the content of another StringJoiner into the current one. The merged content is treated as a single element -- only the content (without the other joiner's prefix and suffix) is added, using the current joiner's delimiter.
import java.util.StringJoiner;
public class MergeDemo {
public static void main(String[] args) {
// Two separate joiners
StringJoiner fruits = new StringJoiner(", ", "[", "]");
fruits.add("Apple");
fruits.add("Banana");
StringJoiner veggies = new StringJoiner(", ", "[", "]");
veggies.add("Carrot");
veggies.add("Pea");
System.out.println("Fruits: " + fruits.toString());
System.out.println("Veggies: " + veggies.toString());
// Output:
// Fruits: [Apple, Banana]
// Veggies: [Carrot, Pea]
// Merge veggies INTO fruits
// Note: veggies' prefix/suffix are dropped, content is added
fruits.merge(veggies);
System.out.println("Merged: " + fruits.toString());
// Output: Merged: [Apple, Banana, Carrot, Pea]
// Merging an empty joiner has no effect
StringJoiner empty = new StringJoiner(", ");
StringJoiner nonEmpty = new StringJoiner(", ");
nonEmpty.add("Hello");
nonEmpty.merge(empty);
System.out.println("After merging empty: " + nonEmpty.toString());
// Output: After merging empty: Hello
// Practical: merge results from multiple data sources
StringJoiner localUsers = new StringJoiner(", ");
localUsers.add("Alice");
localUsers.add("Bob");
StringJoiner remoteUsers = new StringJoiner(", ");
remoteUsers.add("Charlie");
remoteUsers.add("Diana");
StringJoiner allUsers = new StringJoiner(", ", "Users: [", "]");
allUsers.merge(localUsers);
allUsers.merge(remoteUsers);
System.out.println(allUsers.toString());
// Output: Users: [Alice, Bob, Charlie, Diana]
}
}
Java 8 also added the String.join() static method, which internally uses StringJoiner. It is the simplest way to join strings when you do not need a prefix, suffix, or incremental building.
String.join() has two overloads:
String.join(delimiter, elements...) -- join varargsString.join(delimiter, Iterable) -- join any Iterable (List, Set, etc.)import java.util.List;
import java.util.Set;
import java.util.TreeSet;
public class StringJoinDemo {
public static void main(String[] args) {
// Join varargs
String result1 = String.join(", ", "Alice", "Bob", "Charlie");
System.out.println(result1);
// Output: Alice, Bob, Charlie
// Join a List
List languages = List.of("Java", "Python", "Go");
String result2 = String.join(" | ", languages);
System.out.println(result2);
// Output: Java | Python | Go
// Join a Set (order depends on Set implementation)
Set sorted = new TreeSet<>(Set.of("Banana", "Apple", "Cherry"));
String result3 = String.join(" -> ", sorted);
System.out.println(result3);
// Output: Apple -> Banana -> Cherry
// Building file paths
String path = String.join("/", "home", "user", "documents", "report.txt");
System.out.println(path);
// Output: home/user/documents/report.txt
// Building CSS class list
String classes = String.join(" ", "btn", "btn-primary", "btn-lg", "active");
System.out.println("class=\"" + classes + "\"");
// Output: class="btn btn-primary btn-lg active"
// Empty collection: returns empty string
String empty = String.join(", ", List.of());
System.out.println("Empty: '" + empty + "'");
// Output: Empty: ''
}
}
When working with Streams, Collectors.joining() is the preferred way to join elements. It supports delimiter, prefix, and suffix -- the same features as StringJoiner -- but integrates directly into the Stream pipeline. Internally, it uses StringJoiner.
import java.util.List;
import java.util.stream.Collectors;
public class CollectorsJoiningDemo {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie", "Diana");
// Simple delimiter
String csv = names.stream()
.collect(Collectors.joining(", "));
System.out.println(csv);
// Output: Alice, Bob, Charlie, Diana
// Delimiter + prefix + suffix
String json = names.stream()
.map(n -> "\"" + n + "\"")
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(json);
// Output: ["Alice", "Bob", "Charlie", "Diana"]
// Practical: build SQL IN clause from a list of IDs
List ids = List.of(101, 205, 308, 412);
String sqlIn = ids.stream()
.map(String::valueOf)
.collect(Collectors.joining(", ", "SELECT * FROM users WHERE id IN (", ")"));
System.out.println(sqlIn);
// Output: SELECT * FROM users WHERE id IN (101, 205, 308, 412)
// Filter + transform + join
List emails = List.of(
"alice@example.com", "bob@test.org",
"admin@example.com", "diana@test.org"
);
String exampleDomainUsers = emails.stream()
.filter(e -> e.endsWith("@example.com"))
.map(e -> e.substring(0, e.indexOf("@")))
.map(String::toUpperCase)
.collect(Collectors.joining(", "));
System.out.println("Example.com users: " + exampleDomainUsers);
// Output: Example.com users: ALICE, ADMIN
// No-arg joining(): concatenates without delimiter
String together = names.stream()
.collect(Collectors.joining());
System.out.println(together);
// Output: AliceBobCharlieDiana
}
}
StringJoiner and StringBuilder both build strings incrementally, but they serve different purposes. Here is when to use each.
| Feature | StringJoiner | StringBuilder |
|---|---|---|
| Purpose | Building delimited strings | General string building |
| Delimiter handling | Automatic | Manual |
| Prefix/suffix | Built-in | Manual |
| Merge support | Yes (merge()) |
No |
| Empty value | Configurable | Returns "" |
| Index access | No | Yes (charAt, insert, delete) |
| Flexibility | Limited to delimited joining | Full string manipulation |
| Stream support | Via Collectors.joining() |
Manual |
| Best for | CSV, SQL, JSON, log output | Complex string assembly, templates |
Rule of thumb: If you are joining elements with a delimiter, use StringJoiner (or String.join() / Collectors.joining()). If you need fine-grained control like inserting at specific positions, deleting characters, or building non-delimited strings, use StringBuilder.
import java.util.List;
import java.util.StringJoiner;
public class JoinerVsBuilder {
public static void main(String[] args) {
List items = List.of("Apple", "Banana", "Cherry");
// StringBuilder approach: manual delimiter logic
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < items.size(); i++) {
sb.append(items.get(i));
if (i < items.size() - 1) {
sb.append(", ");
}
}
sb.append("]");
System.out.println("StringBuilder: " + sb.toString());
// Output: StringBuilder: [Apple, Banana, Cherry]
// StringJoiner approach: automatic delimiter, prefix, suffix
StringJoiner sj = new StringJoiner(", ", "[", "]");
items.forEach(sj::add);
System.out.println("StringJoiner: " + sj.toString());
// Output: StringJoiner: [Apple, Banana, Cherry]
// StringBuilder is better for non-delimited building
StringBuilder html = new StringBuilder();
html.append("\n");
html.append(" ").append("Title").append("
\n");
html.append(" ").append("Description").append("
\n");
html.append("");
System.out.println(html.toString());
}
}
These examples show StringJoiner solving real-world problems you will encounter in production Java code.
import java.util.List;
import java.util.StringJoiner;
public class CsvGenerator {
public static void main(String[] args) {
// Generate CSV with header and data rows
StringJoiner csv = new StringJoiner("\n");
// Header row
csv.add(String.join(",", "Name", "Age", "City", "Email"));
// Data rows
String[][] data = {
{"Alice", "30", "New York", "alice@example.com"},
{"Bob", "25", "London", "bob@test.org"},
{"Charlie", "35", "Tokyo", "charlie@mail.com"}
};
for (String[] row : data) {
csv.add(String.join(",", row));
}
System.out.println(csv.toString());
// Output:
// Name,Age,City,Email
// Alice,30,New York,alice@example.com
// Bob,25,London,bob@test.org
// Charlie,35,Tokyo,charlie@mail.com
}
}
import java.util.List;
import java.util.StringJoiner;
public class SqlBuilder {
public static void main(String[] args) {
// INSERT statement
List columns = List.of("name", "email", "age", "city");
List values = List.of("'Alice'", "'alice@example.com'", "30", "'New York'");
StringJoiner colJoiner = new StringJoiner(", ", "(", ")");
columns.forEach(colJoiner::add);
StringJoiner valJoiner = new StringJoiner(", ", "(", ")");
values.forEach(valJoiner::add);
String insert = "INSERT INTO users " + colJoiner + " VALUES " + valJoiner;
System.out.println(insert);
// Output: INSERT INTO users (name, email, age, city) VALUES ('Alice', 'alice@example.com', 30, 'New York')
// WHERE clause with multiple conditions
StringJoiner where = new StringJoiner(" AND ", "WHERE ", "");
where.add("status = 'ACTIVE'");
where.add("age >= 18");
where.add("city IN ('New York', 'London')");
System.out.println("SELECT * FROM users " + where);
// Output: SELECT * FROM users WHERE status = 'ACTIVE' AND age >= 18 AND city IN ('New York', 'London')
// UPDATE SET clause
StringJoiner setClause = new StringJoiner(", ", "SET ", "");
setClause.add("name = 'Bob'");
setClause.add("email = 'bob@new.com'");
setClause.add("updated_at = NOW()");
System.out.println("UPDATE users " + setClause + " WHERE id = 1");
// Output: UPDATE users SET name = 'Bob', email = 'bob@new.com', updated_at = NOW() WHERE id = 1
}
}
import java.time.LocalDateTime;
import java.util.StringJoiner;
public class LogFormatter {
public static void main(String[] args) {
// Structured log line: timestamp | level | class | message
StringJoiner log1 = new StringJoiner(" | ");
log1.add(LocalDateTime.now().toString());
log1.add("INFO");
log1.add("UserService");
log1.add("User alice@example.com logged in");
System.out.println(log1.toString());
// Output: 2026-02-28T10:30:00.123 | INFO | UserService | User alice@example.com logged in
// Key-value pairs for structured logging
StringJoiner log2 = new StringJoiner(", ", "{", "}");
log2.add("\"action\": \"login\"");
log2.add("\"user\": \"alice\"");
log2.add("\"ip\": \"192.168.1.1\"");
log2.add("\"success\": true");
System.out.println(log2.toString());
// Output: {"action": "login", "user": "alice", "ip": "192.168.1.1", "success": true}
}
}
This example builds a report generator that can output data in three different formats -- CSV, JSON, and HTML -- all using StringJoiner. It demonstrates every concept covered in this tutorial.
import java.util.List;
import java.util.StringJoiner;
import java.util.stream.Collectors;
class Employee {
private final String name;
private final String department;
private final double salary;
Employee(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
String getName() { return name; }
String getDepartment() { return department; }
double getSalary() { return salary; }
}
class ReportGenerator {
// === CSV FORMAT ===
String generateCsv(List employees) {
StringJoiner report = new StringJoiner("\n");
report.setEmptyValue("No employee data available");
// Header row using String.join()
report.add(String.join(",", "Name", "Department", "Salary"));
// Data rows
for (Employee emp : employees) {
StringJoiner row = new StringJoiner(",");
row.add(emp.getName());
row.add(emp.getDepartment());
row.add(String.format("%.2f", emp.getSalary()));
report.add(row.toString());
}
return report.toString();
}
// === JSON FORMAT ===
String generateJson(List employees) {
// Each employee as a JSON object using StringJoiner
String items = employees.stream()
.map(emp -> {
StringJoiner obj = new StringJoiner(", ", " {", "}");
obj.add("\"name\": \"" + emp.getName() + "\"");
obj.add("\"department\": \"" + emp.getDepartment() + "\"");
obj.add("\"salary\": " + emp.getSalary());
return obj.toString();
})
.collect(Collectors.joining(",\n", "[\n", "\n]"));
return items;
}
// === HTML TABLE FORMAT ===
String generateHtml(List employees) {
StringJoiner table = new StringJoiner("\n");
table.setEmptyValue("No employees found.
");
table.add("");
// Header using StringJoiner with prefix/suffix
StringJoiner header = new StringJoiner("", " ", " ");
header.add("Name");
header.add("Department");
header.add("Salary");
table.add(header.toString());
// Data rows
for (Employee emp : employees) {
StringJoiner row = new StringJoiner("", " ", " ");
row.add(emp.getName());
row.add(emp.getDepartment());
row.add(String.format("$%.2f", emp.getSalary()));
table.add(row.toString());
}
table.add("
");
return table.toString();
}
// === SUMMARY using merge() ===
String generateSummary(List employees) {
// Merge department groups
StringJoiner engineering = new StringJoiner(", ");
StringJoiner marketing = new StringJoiner(", ");
StringJoiner other = new StringJoiner(", ");
for (Employee emp : employees) {
switch (emp.getDepartment()) {
case "Engineering" -> engineering.add(emp.getName());
case "Marketing" -> marketing.add(emp.getName());
default -> other.add(emp.getName());
}
}
StringJoiner summary = new StringJoiner("\n");
summary.add("=== Department Summary ===");
StringJoiner engLine = new StringJoiner(", ", "Engineering: ", "");
engLine.merge(engineering);
summary.add(engLine.toString());
StringJoiner mktLine = new StringJoiner(", ", "Marketing: ", "");
mktLine.merge(marketing);
summary.add(mktLine.toString());
// Total salary using Collectors.joining for labels
String totalLine = "Total employees: " + employees.size();
summary.add(totalLine);
double totalSalary = employees.stream()
.mapToDouble(Employee::getSalary)
.sum();
summary.add(String.format("Total salary: $%.2f", totalSalary));
return summary.toString();
}
}
public class ReportApp {
public static void main(String[] args) {
List employees = List.of(
new Employee("Alice", "Engineering", 95000),
new Employee("Bob", "Marketing", 72000),
new Employee("Charlie", "Engineering", 105000),
new Employee("Diana", "Marketing", 68000),
new Employee("Eve", "Engineering", 115000)
);
ReportGenerator generator = new ReportGenerator();
// CSV Report
System.out.println("=== CSV Report ===");
System.out.println(generator.generateCsv(employees));
System.out.println();
// JSON Report
System.out.println("=== JSON Report ===");
System.out.println(generator.generateJson(employees));
System.out.println();
// HTML Report
System.out.println("=== HTML Report ===");
System.out.println(generator.generateHtml(employees));
System.out.println();
// Summary with merge()
System.out.println(generator.generateSummary(employees));
System.out.println();
// Empty report: demonstrates setEmptyValue
System.out.println("=== Empty CSV Report ===");
System.out.println(generator.generateCsv(List.of()));
System.out.println();
System.out.println("=== Empty HTML Report ===");
System.out.println(generator.generateHtml(List.of()));
}
}
// Output:
// === CSV Report ===
// Name,Department,Salary
// Alice,Engineering,95000.00
// Bob,Marketing,72000.00
// Charlie,Engineering,105000.00
// Diana,Marketing,68000.00
// Eve,Engineering,115000.00
//
// === JSON Report ===
// [
// {"name": "Alice", "department": "Engineering", "salary": 95000.0},
// {"name": "Bob", "department": "Marketing", "salary": 72000.0},
// {"name": "Charlie", "department": "Engineering", "salary": 105000.0},
// {"name": "Diana", "department": "Marketing", "salary": 68000.0},
// {"name": "Eve", "department": "Engineering", "salary": 115000.0}
// ]
//
// === HTML Report ===
//
// Name Department Salary
// Alice Engineering $95000.00
// Bob Marketing $72000.00
// Charlie Engineering $105000.00
// Diana Marketing $68000.00
// Eve Engineering $115000.00
//
//
// === Department Summary ===
// Engineering: Alice, Charlie, Eve
// Marketing: Bob, Diana
// Total employees: 5
// Total salary: $455000.00
//
// === Empty CSV Report ===
// No employee data available
//
// === Empty HTML Report ===
// No employees found.
| Concept | Where Used |
|---|---|
StringJoiner(delimiter) |
CSV row builder, department grouping |
StringJoiner(delimiter, prefix, suffix) |
JSON objects, HTML table rows, SQL-style output |
add() |
Every joiner instance |
merge() |
Department summary -- merging grouped joiners |
setEmptyValue() |
CSV and HTML empty report fallback |
String.join() |
CSV header row |
Collectors.joining() |
JSON array with stream pipeline |
| Stream + StringJoiner | JSON generation with map + collect |
| Method / Class | Description | Example |
|---|---|---|
new StringJoiner(delim) |
Create with delimiter only | new StringJoiner(", ") |
new StringJoiner(delim, prefix, suffix) |
Create with delimiter, prefix, suffix | new StringJoiner(", ", "[", "]") |
add(element) |
Append an element | joiner.add("Alice") |
merge(other) |
Merge another joiner's content | joiner.merge(otherJoiner) |
setEmptyValue(value) |
Custom string when no elements added | joiner.setEmptyValue("N/A") |
toString() |
Get the joined result | joiner.toString() |
length() |
Length of the current result | joiner.length() |
String.join(delim, elements...) |
Static join with varargs | String.join(", ", "A", "B") |
String.join(delim, iterable) |
Static join with Iterable | String.join(", ", list) |
Collectors.joining() |
Stream collector, no delimiter | stream.collect(Collectors.joining()) |
Collectors.joining(delim) |
Stream collector with delimiter | stream.collect(Collectors.joining(", ")) |
Collectors.joining(delim, pre, suf) |
Stream collector with all options | stream.collect(Collectors.joining(", ", "[", "]")) |