Definition
- joining(delimiter) inserts the delimiter between elements, never before the first or after the last.
- joining(delimiter, prefix, suffix) additionally wraps the result in a prefix and suffix.
- No-argument joining() concatenates with no separator at all.
- Requires CharSequence elements, so non-string types need
map(String::valueOf)first. - Uses StringBuilder internally, making it efficient even for many elements.
Priya at the National Bank of India has a list of account holder names and needs them on a single summary-header line separated by commas. She glues them together with a comma and space between each, producing a clean line like Rajesh, Priya, Amit with no stray commas.
Understanding the Concepts
What does this question ask?
The interviewer wants you to join a list of strings into one delimited string. This tests Collectors.joining, its overloaded forms, and handling of non-string elements.
How do you join strings with a comma delimiter?
package com.bank.service;
import java.util.List;
import java.util.stream.Collectors;
public class JoiningDemo {
public static void main(String[] args) {
List<String> holders = List.of("Rajesh", "Priya", "Amit", "Deepak");
String result = holders.stream()
.collect(Collectors.joining(", "));
System.out.println(result);
}
}
Rajesh, Priya, Amit, Deepak
The delimiter appears only between elements, never at the start or end.
How does the three-argument form work?
String bracketed = holders.stream()
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(bracketed);
[Rajesh, Priya, Amit, Deepak]
The prefix and suffix wrap the joined result, which is useful for producing JSON-like or display formats.
How do you join non-string elements?
joining requires Stream. For other types, map to String first.
package com.bank.service;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
public class JoiningNonString {
public static void main(String[] args) {
List<BigDecimal> balances = List.of(
new BigDecimal("50000.00"),
new BigDecimal("125000.50"),
new BigDecimal("75000.25"));
String csv = balances.stream()
.map(BigDecimal::toPlainString)
.collect(Collectors.joining(", "));
System.out.println("Balances: " + csv);
}
}
Balances: 50000.00, 125000.50, 75000.25
What happens with an empty stream?
List<String> empty = List.of();
String noArgs = empty.stream().collect(Collectors.joining());
String withDelim = empty.stream().collect(Collectors.joining(", "));
String withWrap = empty.stream().collect(Collectors.joining(", ", "[", "]"));
System.out.println("No args: '" + noArgs + "'");
System.out.println("Delim: '" + withDelim + "'");
System.out.println("Wrap: '" + withWrap + "'");
No args: ''
Delim: ''
Wrap: '[]'
The one- and two-argument forms return an empty string. The three-argument form returns just the prefix and suffix.
How is joining different from String.join?
| Feature | Collectors.joining | String.join |
|---|---|---|
| Works with | Stream pipeline | Array or Iterable directly |
| Prefix/suffix | Yes (three-argument form) | No |
| Pipeline integration | Yes, as a terminal collector | No, standalone static method |
// String.join: standalone, no prefix/suffix
String sj = String.join(", ", holders);
// Collectors.joining: inside a pipeline with map/filter
String cj = holders.stream()
.filter(name -> name.length() > 4)
.collect(Collectors.joining(", "));
What are common mistakes?
Mistake 1: using manual loops with separator flags.
// WRONG: error-prone manual loop
List<String> names = List.of("Rajesh", "Priya");
StringBuilder sb = new StringBuilder();
for (int i = 0; i < names.size(); i++) {
if (i > 0) sb.append(", ");
sb.append(names.get(i));
}
// Works but verbose and easy to get wrong
// CORRECT: joining handles it
String result = names.stream().collect(Collectors.joining(", "));
Mistake 2: assuming joining works on non-CharSequence streams.
// WRONG: compile error, Integer is not CharSequence
// List.of(1, 2, 3).stream().collect(Collectors.joining(", "));
// CORRECT: map to String first
String nums = List.of(1, 2, 3).stream()
.map(String::valueOf)
.collect(Collectors.joining(", "));
What about CSV output with escaping?
Plain joining does not handle values that contain the delimiter character. For proper CSV encoding, you need to escape or quote such values.
List<String> values = List.of("Rajesh, Kumar", "Priya", "Amit");
// WRONG: produces ambiguous CSV
String bad = values.stream().collect(Collectors.joining(","));
// "Rajesh, Kumar,Priya,Amit" -> 4 fields instead of 3
// CORRECT: quote values containing the delimiter
String good = values.stream()
.map(v -> v.contains(",") ? "\"" + v + "\"" : v)
.collect(Collectors.joining(","));
// "\"Rajesh, Kumar\",Priya,Amit" -> 3 fields
What are common interview follow-up questions?
- "How do you split a comma-separated string back into a list?" Use
Arrays.asList(s.split(", "))orPattern.compile(", ").splitAsStream(s). - "How do you join with a newline?" Use
joining("\n")for line-separated output. - "How do you build a SQL IN clause?" Use
joining(", ", "IN (", ")")after mapping values to quoted strings.
Visual Overview
List: ["Rajesh", "Priya", "Amit", "Deepak"]
│
│ .stream()
│ .collect(Collectors.joining(", "))
▼
┌───────────────────────────────────────┐
│ "Rajesh" + ", " + │
│ "Priya" + ", " + │
│ "Amit" + ", " + │
│ "Deepak" │
└───────────────────────────────────────┘
│
▼
"Rajesh, Priya, Amit, Deepak"
Three-argument: joining(", ", "[", "]")
▼
"[Rajesh, Priya, Amit, Deepak]"
Key Takeaway
Collectors.joining is the idiomatic way to turn a list of strings into a single delimited line. The one-argument form inserts the delimiter only between elements, never before the first or after the last. The three-argument form additionally wraps the result with a prefix and suffix, which is useful for bracketed or parenthesized output.
Since joining operates on a Stream of CharSequence, non-string elements like BigDecimal balances must be mapped to String first. Internally it uses a StringBuilder, so it is efficient even for large collections. At the National Bank of India, Rajesh used this to render account holder names as a comma-separated summary header.
The senior point is that joining replaces error-prone manual loops with separator flags or trailing-comma trimming. It also handles the empty-stream edge case correctly: an empty stream produces an empty string, or just the prefix and suffix with the three-argument form. For CSV output where values might contain the delimiter, joining alone is not enough and you need quoting or escaping.
Remember one sentence: Collectors.joining inserts the delimiter only between elements, uses StringBuilder internally, and replaces manual separator logic.
---