Back to Collectors
Q11Question 11 of 21

StringJoiner: delimiter, prefix, suffix: relationship to Collectors.joining and String.join

Key Concepts
  • StringJoiner is a mutable builder for constructing delimited strings with optional prefix and suffix
  • String.join(delimiter, elements) is a static convenience for simple cases without prefix/suffix
  • Collectors.joining(delimiter, prefix, suffix) is the stream-collector version
  • All three use StringJoiner internally and produce identical results
  • StringJoiner.setEmptyValue overrides the default output for empty joins
  • StringJoiner.merge combines two joiners for parallel stream processing
  • Choose based on context: imperative loop uses StringJoiner, static helper uses String.join, stream uses Collectors.joining

Definition

StringJoiner is the underlying builder for delimited string concatenation; String.join and Collectors.joining are convenience layers on top of it.
  • StringJoiner – new StringJoiner(", ", "[", "]") builds a delimited string with prefix and suffix. Use add() to append elements.
  • String.join – String.join(", ", list) is a static convenience for the common no-prefix/suffix case.
  • Collectors.joining – Collectors.joining(", ", "[", "]") is the stream collector version.
  • Relationship – all three use StringJoiner internally and produce identical results.
  • setEmptyValue – overrides the default empty output (prefix+suffix) with a custom message.

StringJoiner is the bank's form printer: you feed account IDs one by one and it produces a formatted list. String.join is a quick-print button for simple lists. Collectors.joining is the same printer connected to the stream conveyor belt.

Understanding the Concepts

What is StringJoiner?

JAVA
import java.util.StringJoiner;

StringJoiner sj = new StringJoiner(", ", "[", "]");
sj.add("SAV2001");
sj.add("CUR3001");
sj.add("FD4001");
System.out.println(sj.toString());  // [SAV2001, CUR3001, FD4001]

// Empty StringJoiner with setEmptyValue:
StringJoiner empty = new StringJoiner(", ", "[", "]");
empty.setEmptyValue("[no accounts]");
System.out.println(empty.toString());  // [no accounts]

How do the three options relate?

JAVA
import java.util.List;
import java.util.stream.Collectors;

List<String> accounts = List.of("SAV2001", "CUR3001", "FD4001");

// Option 1: StringJoiner (imperative)
StringJoiner sj = new StringJoiner(", ", "[", "]");
for (String id : accounts) sj.add(id);
String result1 = sj.toString();

// Option 2: String.join (static helper, no prefix/suffix)
String result2 = String.join(", ", accounts);

// Option 3: Collectors.joining (stream)
String result3 = accounts.stream()
    .collect(Collectors.joining(", ", "[", "]"));

// All produce the same formatted string.

What is the comparison table?

| Context | Use | Prefix/Suffix | Empty fallback |
|---|---|:---:|:---:|
| Imperative loop with conditions | StringJoiner | Yes | setEmptyValue |
| Simple delimiter join | String.join | No | N/A |
| Stream pipeline | Collectors.joining | Yes | N/A |

How does StringJoiner handle conditional adds?

JAVA
package com.bank.service;

import java.util.List;
import java.util.StringJoiner;

public class ConditionalJoin {
    record Account(String id, boolean active) {}

    public static void main(String[] args) {
        List<Account> accounts = List.of(
            new Account("SAV2001", true),
            new Account("SAV2002", false),
            new Account("CUR3001", true));

        // StringJoiner with condition (imperative)
        StringJoiner sj = new StringJoiner(", ", "Active: [", "]");
        for (Account a : accounts) {
            if (a.active()) sj.add(a.id());
        }
        System.out.println(sj.toString()); // Active: [SAV2001, CUR3001]

        // Stream equivalent
        String result = accounts.stream()
            .filter(Account::active)
            .map(Account::id)
            .collect(java.util.stream.Collectors.joining(", ", "Active: [", "]"));
        System.out.println(result); // Active: [SAV2001, CUR3001]
    }
}

How does merge work for parallel streams?

StringJoiner.merge(other) combines two joiners, which is how Collectors.joining handles parallel streams.

JAVA
StringJoiner sj1 = new StringJoiner(", ");
sj1.add("SAV2001").add("SAV2002");

StringJoiner sj2 = new StringJoiner(", ");
sj2.add("CUR3001").add("CUR3002");

sj1.merge(sj2);
System.out.println(sj1.toString()); // SAV2001, SAV2002, CUR3001, CUR3002

How does setEmptyValue work?

JAVA
StringJoiner sj = new StringJoiner(", ", "[", "]");
sj.setEmptyValue("[no results]");

// Without any adds:
System.out.println(sj.toString()); // [no results]

// With adds:
sj.add("SAV2001");
System.out.println(sj.toString()); // [SAV2001]

Without setEmptyValue, an empty joiner returns just prefix+suffix: [].

What are common mistakes?

JAVA
// WRONG: using + concatenation in a loop
String result = "";
for (String id : accounts) {
    result += id + ", "; // O(n^2) string copying!
}
// Trailing comma, poor performance

// CORRECT: StringJoiner or String.join
String result = String.join(", ", accounts); // O(n), no trailing comma

What should a senior engineer know?

  • StringJoiner.merge(other) combines two joiners, which is how Collectors.joining handles parallel streams.
  • String.join uses StringJoiner internally: the implementation is new StringJoiner(delimiter).add(elements)...toString().
  • For conditional adds (only join non-empty strings), StringJoiner with an if-check is cleaner than stream+filter+joining.
  • StringJoiner is NOT thread-safe. Use Collectors.joining for parallel streams, which handles merging internally.
  • setEmptyValue replaces the default prefix+suffix output for empty joiners with a custom message.

Visual Overview

DIAGRAM
  Three Layers:

  ┌───────────────────────────────────┐
  │   Collectors.joining(", ","[","]")│  ◀── Stream pipeline
  │   (stream collector)              │
  ├───────────────────────────────────┤
  │   String.join(", ", list)         │  ◀── Static helper
  │   (convenience method)            │
  ├───────────────────────────────────┤
  │   StringJoiner(", ", "[", "]")   │  ◀── Underlying builder
  │   .add("SAV2001").add("CUR3001") │
  └───────────────────────────────────┘
  All use StringJoiner internally.

Key Takeaway

StringJoiner is the underlying builder for delimited string concatenation. String.join is a static shortcut for simple cases. Collectors.joining is the stream collector. All three produce identical results because they all use StringJoiner internally.

Choose based on context: imperative loops use StringJoiner directly (especially when you need conditional adds or setEmptyValue), static helper calls use String.join, and stream pipelines use Collectors.joining. StringJoiner.merge combines two joiners, which is how Collectors.joining handles parallel stream processing.

Remember one sentence: StringJoiner is the builder underneath both String.join and Collectors.joining; choose the layer that matches your context.

---

Share
Use ← → arrow keys to navigate