Definition
Collectors.joining(delimiter)concatenates a stream of strings with the given separator between each pair.- Three overloads: no-arg (plain concat), one-arg (delimiter), three-arg (delimiter, prefix, suffix).
- Internally uses
StringJoiner, which appends to a single buffer for O(n) performance. - No trailing delimiter: the separator appears only between elements, not at the start or end.
- Requires
Stream: you must map to strings first.
The office manager at the National Bank of India wants to send one email to all staff, so she needs every address strung together, separated by commas, to paste into the To box. joining(", ") is exactly that stringing-together.
Understanding the Concepts
What does Collectors.joining do?
First you project each employee to its email with map(Employee::getEmail), producing a stream of email strings. Then the terminal collector Collectors.joining(", ") concatenates them into a single string, inserting the delimiter between consecutive elements but not at the very start or very end.
How do you extract emails as a comma-separated string?
package com.bank.hr;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
public class JoiningEmails {
public static void main(String[] args) {
List<Employee> staff = List.of(
new Employee("Rajesh", "rajesh@nbi.com", "Loans", new BigDecimal("95000")),
new Employee("Priya", "priya@nbi.com", "IT", new BigDecimal("120000")),
new Employee("Amit", "amit@nbi.com", "Finance", new BigDecimal("85000")));
String emailList = staff.stream()
.map(Employee::getEmail)
.collect(Collectors.joining(", "));
System.out.println(emailList);
}
}
rajesh@nbi.com, priya@nbi.com, amit@nbi.com
What are the three overloads of joining?
| Overload | Example | Result |
|---|---|---|
| joining() | joining() | "rajesh@nbi.compriya@nbi.comamit@nbi.com" |
| joining(delimiter) | joining(", ") | "rajesh@nbi.com, priya@nbi.com, amit@nbi.com" |
| joining(delim, prefix, suffix) | joining(", ", "[", "]") | "[rajesh@nbi.com, priya@nbi.com, amit@nbi.com]" |
// Bracketed list for a summary report
String bracketed = staff.stream()
.map(Employee::getEmail)
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(bracketed);
// [rajesh@nbi.com, priya@nbi.com, amit@nbi.com]
What are common mistakes?
Mistake: a null email in the stream throws NullPointerException during concatenation.
// WRONG: if any email is null, joining crashes
String emails = staff.stream()
.map(Employee::getEmail) // might return null
.collect(Collectors.joining(", ")); // NPE!
// CORRECT: filter out nulls first
String emails = staff.stream()
.map(Employee::getEmail)
.filter(java.util.Objects::nonNull)
.collect(Collectors.joining(", "));
How is joining different from reduce for string concatenation?
| Approach | Performance | Simplicity |
|---|---|---|
| Collectors.joining(", ") | O(n) via StringJoiner | One line, clean |
| reduce("", (a, b) -> a + ", " + b) | O(n^2) via String copies | Bug-prone (leading comma) |// Joining: O(n), clean, no leading-comma bug
staff.stream().map(Employee::getEmail)
.collect(Collectors.joining(", "));
// Reduce: O(n^2), must guard leading comma manually
staff.stream().map(Employee::getEmail)
.reduce("", (a, b) -> a.isEmpty() ? b : a + ", " + b);
What about joining non-string types?
joinig only works on Stream. For non-string types, map to a string representation first.
// Joining salary amounts as a string
String salaries = staff.stream()
.map(e -> e.getSalary().toPlainString())
.collect(Collectors.joining(" | "));
System.out.println(salaries);
// 95000 | 120000 | 85000
What should a senior engineer know?
- Under the hood,
joininguses aStringJoiner, which is efficient because it appends to a single growing buffer rather than creating a new string for each concatenation. - For very large datasets, the resulting string could be huge; consider streaming output instead of building one giant string.
- joining is the idiomatic and efficient way to build delimited strings; never use
reducewith string concatenation in production.
What are common interview follow-up questions?
- "What happens if the stream is empty?" joining returns an empty string (or just prefix+suffix for the three-arg form).
- "Why not use reduce for string concatenation?" It is O(n^2) because each + creates a new String.
- "Can you add a prefix and suffix?" Yes, use the three-argument joining(delimiter, prefix, suffix).
Visual Overview
Stream<Employee>
[Rajesh, Priya, Amit]
│
│ .map(Employee::getEmail)
▼
Stream<String>
["rajesh@nbi.com", "priya@nbi.com", "amit@nbi.com"]
│
│ .collect(Collectors.joining(", "))
│
│ StringJoiner buffer:
│ "rajesh@nbi.com"
│ "rajesh@nbi.com, priya@nbi.com"
│ "rajesh@nbi.com, priya@nbi.com, amit@nbi.com"
▼
String: "rajesh@nbi.com, priya@nbi.com, amit@nbi.com"
Key Takeaway
To turn a stream of strings into one delimited string I use the joining collector, giving it the delimiter I want between elements. Here I first mapped each employee to its email, then collected with joining(", "), which concatenates all the addresses with a comma and space between each, and no delimiter before the first or after the last.
In my banking project at the National Bank of India this built the recipient line for a staff circular, where the office manager needed every address in one comma-separated string. The three-argument form with prefix and suffix is handy for building a bracketed summary.
joining uses a StringJoiner internally, so it appends to one growing buffer for O(n) performance, unlike reduce with string concatenation which is O(n^2). A null in the stream throws NullPointerException, so I always filter nulls before joining.
Remember one sentence: Collectors.joining(", ") concatenates a stream of strings efficiently using a StringJoiner, with the delimiter only between elements.
---