StringJoiner, String.join() and Collectors.joining()
StringJoiner assembles a sequence of strings with a delimiter, an optional prefix, and an optional suffix, giving you [a, b, c] or comma-separated values without manual StringBuilder bookkeeping. String.join() is the one-line shorthand for the common case. Collectors.joining() brings the same capability to streams. This article covers all three with practical examples and guidance on when to choose each.
The Problem StringJoiner Solves
Joining a list of values with a separator is one of the most common string-building tasks in Java, generating a CSV row, building a SQL IN clause, formatting a list for a log message. Before Java 8, the only way to do it without a library was a StringBuilder
// Pre-Java-8 pattern — error-prone trailing-delimiter problem
List<String> names = List.of("Alice", "Bob", "Carol");
StringBuilder sb = new StringBuilder();
for (String name : names) {
sb.append(name).append(", "); // adds trailing comma after last element!
}
// Must strip the trailing ", " — easy to forget or get wrong
String result = sb.substring(0, sb.length() - 2);
System.out.println(result); // Alice, Bob, Carol
The pattern has two failure modes: forgetting to trim the trailing delimiter, and mishandling the edge case where the list is empty. StringJoiner
Basic StringJoiner Usage
StringJoinerjava.utiladd()toString()toString()setEmptyValue()
import java.util.StringJoiner;
// Simple delimiter
StringJoiner sj = new StringJoiner(", ");
sj.add("Alice");
sj.add("Bob");
sj.add("Carol");
System.out.println(sj); // Alice, Bob, Carol
// With prefix and suffix
StringJoiner bracketed = new StringJoiner(", ", "[", "]");
bracketed.add("one");
bracketed.add("two");
bracketed.add("three");
System.out.println(bracketed); // [one, two, three]
// SQL IN clause
StringJoiner inClause = new StringJoiner(", ", "IN (", ")");
inClause.add("'NY'");
inClause.add("'LA'");
inClause.add("'CH'");
System.out.println(inClause); // IN ('NY', 'LA', 'CH')
// Empty value — returned when no elements are added
StringJoiner empty = new StringJoiner(", ", "[", "]");
empty.setEmptyValue("(none)");
System.out.println(empty); // (none)
empty.add("x");
System.out.println(empty); // [x] — prefix/suffix appear once an element is added
// Merging two joiners
StringJoiner first = new StringJoiner(", ");
first.add("a").add("b");
StringJoiner second = new StringJoiner(", ");
second.add("c").add("d");
first.merge(second);
System.out.println(first); // a, b, c, d
Note: merge(other)other
String.join(): The One-Line Shorthand
String.join()StringJoinerStringJoinertoString()Iterable
// Varargs overload
String csv = String.join(", ", "Alice", "Bob", "Carol");
System.out.println(csv); // Alice, Bob, Carol
// Iterable overload — works with List, Set, any Collection
List<String> cities = List.of("New York", "London", "Tokyo");
String joined = String.join(" | ", cities);
System.out.println(joined); // New York | London | Tokyo
// Joining an array
String[] langs = {"Java", "Kotlin", "Scala"};
System.out.println(String.join(", ", langs)); // Java, Kotlin, Scala
// Splitting and rejoining (e.g., normalising separators)
String path = "/usr/local/bin/java";
String[] parts = path.split("/");
String rejoin = String.join("\\", parts); // Windows-style path
String.join()[a, b, c]a, b, cStringJoiner"[" + String.join(", ", list) + "]"Collectors.joining(): Stream-Based Joining
Collectors.joining()Stream<String>StringJoinerfilter()map()sorted()
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.List;
// 1. No delimiter — simple concatenation
String concat = Stream.of("a", "b", "c").collect(Collectors.joining());
System.out.println(concat); // abc
// 2. Delimiter only
String csv = Stream.of("Alice", "Bob", "Carol")
.collect(Collectors.joining(", "));
System.out.println(csv); // Alice, Bob, Carol
// 3. Delimiter + prefix + suffix
String json = Stream.of("\"Alice\"", "\"Bob\"", "\"Carol\"")
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(json); // ["Alice", "Bob", "Carol"]
// 4. Map fields before joining — common real-world pattern
record Person(String name, int age) {}
List<Person> people = List.of(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Carol", 35)
);
String names = people.stream()
.filter(p -> p.age() >= 30)
.map(Person::name)
.sorted()
.collect(Collectors.joining(", "));
System.out.println(names); // Alice, Carol
// 5. Joining after a transformation
String upperCsv = List.of("java", "kotlin", "scala")
.stream()
.map(String::toUpperCase)
.collect(Collectors.joining(", "));
System.out.println(upperCsv); // JAVA, KOTLIN, SCALA
Choosing Between StringJoiner, String.join(), and Collectors.joining()
All three are backed by the same mechanism and have equivalent performance. The choice is about code clarity and fit with the surrounding context.
Use String.join()java.lang
Use Collectors.joining()
Use StringJoiner
| Scenario | Best choice |
|---|---|
| Join a List or array, delimiter only | String.join() |
| Join inside a stream pipeline | Collectors.joining() |
Need prefix/suffix (e.g., [a, b] | StringJoinerCollectors.joining(d, p, s) |
| Add elements conditionally one by one | StringJoiner |
| Custom empty value | StringJoiner.setEmptyValue() |
| Merge two joiners | StringJoiner.merge() |