String.join()
Joins strings from varargs or any Iterable with a delimiter between them, with no trailing separator to trim off.
public static String join(CharSequence delimiter, Iterable<? extends CharSequence> elements)- Added in Java Java 8
Parameters
| Parameter | Type | Description |
|---|---|---|
$delimiter | CharSequence | Text placed between each pair of elements, not before the first or after the last. |
$elements | Iterable<? extends CharSequence> | The values to join. The overload join(CharSequence, CharSequence...) takes varargs or an array instead. |
Return value
StringA new String of the elements separated by the delimiter. An empty Iterable gives an empty String.
Errors
NullPointerException if delimiter or elements is null. A null element is joined as the text "null".
String.join(delimiter, elements) puts the delimiter between each pair of elements and returns one String. It is a static method with two overloads: one takes varargs (so also a String[]), the other takes any Iterable, such as a List or Set.
String.join(", ", "GET", "POST") // "GET, POST"
String.join("/", List.of("api", "v2")) // "api/v2"Why not a loop?
A loop that appends each value and then a comma leaves a trailing comma that needs to be cut off, and an off-by-one in that fix is a classic bug. String.join handles the first, last and only element for you. An empty collection gives an empty String; a single element gives that element with no delimiter.
Choosing between the join tools
String.join: you already have a collection or array of Strings.Collectors.joining(delim, prefix, suffix): the values come from a stream, often aftermaporfilter, or you need brackets around the result.StringJoiner: you add values one at a time in a loop with conditions, or want a custom text for the empty case viasetEmptyValue.
All three use a StringBuilder-style buffer internally, so they are efficient for large inputs.
Gotchas
Elements must be CharSequences. A List<Integer> does not compile; map it to Strings first, for example with a stream. A null element is not skipped: it appears as the four letters null. Filter nulls and blanks before joining.
Joining is not escaping. If a value can contain the delimiter, the result cannot be split back reliably, which matters for CSV lines, cache keys and log fields. For SQL, join only placeholders such as ?, never values; build the IN (?, ?, ?) list with Collections.nCopies and bind the values on a PreparedStatement.
Example
When to use it
- Building the Allow header for a 405 response from the methods a route supports.
- Generating an IN (?, ?, ?) list for a PreparedStatement with one placeholder per id.
- Writing a list of tags or roles into one comma-separated column or log field.
Frequently asked questions
How do I join a List of Strings with commas in Java?
String.join(",", list). It works with any Iterable of CharSequence and puts the comma only between elements.
How do I join a List of Integers?
String.join needs CharSequence elements, so convert first: list.stream().map(String::valueOf).collect(Collectors.joining(",")).
What is the difference between String.join and Collectors.joining?
String.join works on an existing array or Iterable. Collectors.joining is a stream collector, so it fits after map and filter, and it can add a prefix and suffix.
More examples
Building an Allow header
An EnumSet iterates in declaration order whatever order you add to it, so the header is stable. A 405 Method Not Allowed response must carry this header.
enum Method { GET, POST, PUT, DELETE }
void main() {
Set<Method> allowed = EnumSet.of(Method.DELETE, Method.GET, Method.POST);
List<String> names = allowed.stream().map(Method::name).toList();
IO.println("Allow: " + String.join(", ", names));
}
// Output:
// Allow: GET, POST, DELETESQL placeholders, JSON arrays and StringJoiner
Only the question marks are joined into the SQL; the ids are bound later on a PreparedStatement. Collectors.joining adds a prefix and suffix, and StringJoiner can print a chosen value when nothing was added.
void main() {
List<Integer> ids = List.of(3, 7, 42);
String placeholders = String.join(", ", Collections.nCopies(ids.size(), "?"));
IO.println("SELECT * FROM orders WHERE id IN (" + placeholders + ")");
String json = ids.stream().map(String::valueOf).collect(Collectors.joining(",", "[", "]"));
IO.println(json);
StringJoiner sj = new StringJoiner(", ", "{", "}");
sj.setEmptyValue("none");
IO.println(sj);
}
// Output:
// SELECT * FROM orders WHERE id IN (?, ?, ?)
// [3,7,42]
// noneReplacing a trailing-comma loop
The loop leaves a trailing comma, while join places the delimiter only between elements. Filtering blanks first avoids empty fields such as "java,,api".
void main() {
List<String> tags = List.of("java", "api", "backend");
StringBuilder sb = new StringBuilder();
for (String t : tags) sb.append(t).append(",");
IO.println(sb);
IO.println(String.join(",", tags));
List<String> withBlanks = List.of("java", "", "api", " ");
IO.println(String.join(",", withBlanks.stream().filter(s -> !s.isBlank()).toList()));
}
// Output:
// java,api,backend,
// java,api,backend
// java,api
Discussion