Collectors.joining()

Concatenates stream elements into one String, with an optional delimiter, prefix and suffix.

Signature
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
  • Added in Java Java 8

Parameters

ParameterTypeDescription
$delimiteroptionalCharSequence= "" (joining() has no delimiter)Placed between consecutive elements.
$prefixoptionalCharSequence= ""Placed at the start of the result. Only in the three-argument form.
$suffixoptionalCharSequence= ""Placed at the end of the result. Only in the three-argument form.

Return value

Collector<CharSequence, ?, String>A collector that concatenates the elements in encounter order, separated by the delimiter and wrapped in the prefix and suffix. An empty stream gives prefix + suffix, or an empty string with no prefix or suffix.

Errors

A null delimiter, prefix or suffix is not rejected when the collector is built; collect then throws NullPointerException, even on an empty stream. A null stream element does not throw; it is appended as the text "null".

Collectors.joining concatenates the elements of a stream into one String. There are three forms:

  • joining(): elements back to back, no separator.
  • joining(", "): a delimiter between elements, not after the last one.
  • joining(", ", "[", "]"): a delimiter plus a prefix and suffix around the whole result.

It collects the parts and builds the string once at the end, so it is the right tool instead of += in a loop, and it never leaves a trailing comma to trim off.

It only accepts text

The collector's element type is CharSequence. A Stream<Integer> or a stream of records does not compile with joining; map the elements to strings first with map(String::valueOf) or a field accessor. That compile error is the most common question about this method.

Empty streams and nulls

An empty stream gives an empty string, or just the prefix and suffix: "[]" for joining(", ", "[", "]"). That is usually what you want for a list display, but it can produce broken output such as WHERE id IN (), which is a SQL syntax error, so check for an empty input before building a query. A null element is not an error; it is appended as the four letters null, which tends to show up in user-facing text. Filter nulls out or map them to a default first.

Back-end uses and limits

Typical jobs are log lines, header values such as Allow: GET, POST, CSV rows and SQL placeholder lists like (?, ?, ?). Join placeholders, never the values themselves: values go to PreparedStatement parameters. For CSV and JSON, joining only puts the separators in; quoting and escaping are your job, which is why a JSON library is the better choice once the data is more than a few known-safe strings.

When you already have a List<String>, String.join(", ", list) is shorter. joining wins in the middle of a pipeline, after filter, map or sorted, or as a downstream collector inside groupingBy.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Building the value of an Allow header for a 405 Method Not Allowed response.
  • Generating '(?, ?, ?)' placeholder lists for a SQL IN clause whose values are bound with PreparedStatement.
  • Writing one log line that lists the ids of every record a batch job skipped.

Frequently asked questions

How do I join a list of integers into a comma-separated string?

Map each number to a String first, because joining only accepts CharSequence: ids.stream().map(String::valueOf).collect(Collectors.joining(",")).

What does Collectors.joining return for an empty stream?

An empty string for joining() and joining(delimiter). The three-argument form returns the prefix followed by the suffix, so joining(", ", "[", "]") gives "[]".

Should I use String.join or Collectors.joining?

Use String.join when you already have the strings in a collection or array. Use Collectors.joining inside a stream pipeline, after filter, map or sorted, or as a downstream collector of groupingBy.

More examples

Joining numbers and null elements

map(String::valueOf) converts each number to text so joining accepts it. Nulls are joined as the word null, so filter them out before the text reaches a user or an email template.

Example · java
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

void main() {
    List<Integer> ids = List.of(101, 102, 103);
    // ids.stream().collect(Collectors.joining(",")) does not compile: Integer is not a CharSequence.
    String idList = ids.stream().map(String::valueOf).collect(Collectors.joining(","));
    IO.println(idList);

    List<String> names = Arrays.asList("ada", null, "cy");
    IO.println(names.stream().collect(Collectors.joining(", ")));
    IO.println(names.stream().filter(Objects::nonNull).collect(Collectors.joining(", ")));
}
// Output:
// 101,102,103
// ada, null, cy
// ada, cy

A CSV line with quoted fields

joining only places the commas; quoting a field that contains a comma or a quote, and doubling inner quotes, is done in the map step. Skipping that step shifts every column after the first name with a comma in it.

Example · java
import java.util.List;
import java.util.stream.Collectors;

void main() {
    List<String> fields = List.of("o-17", "Smith, Ada", "say \"hi\"", "2500");

    String line = fields.stream()
        .map(f -> f.contains(",") || f.contains("\"")
            ? "\"" + f.replace("\"", "\"\"") + "\""
            : f)
        .collect(Collectors.joining(","));
    IO.println(line);
}
// Output:
// o-17,"Smith, Ada","say ""hi""",2500

An Allow header from the supported methods

Collectors.joining fits after a filter and map in the same pipeline. String.join is enough when you already hold exactly the strings you want, as with the key set on the last line.

Example · java
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;

void main() {
    Map<String, Boolean> enabled = new TreeMap<>(Map.of(
        "GET", true, "POST", true, "PUT", false, "DELETE", true));

    String allow = enabled.entrySet().stream()
        .filter(Map.Entry::getValue)
        .map(Map.Entry::getKey)
        .collect(Collectors.joining(", "));
    IO.println("Allow: " + allow);

    IO.println("Allow: " + String.join(", ", enabled.keySet()));
}
// Output:
// Allow: DELETE, GET, POST
// Allow: DELETE, GET, POST, PUT

Discussion

  • Be the first to comment on this lesson.