StringJoiner

August 5, 201912 min readUpdated 2/28/2026

1. What is StringJoiner?

StringJoiner is a utility class introduced in Java 8 that builds a sequence of characters separated by a delimiter, with optional prefix and suffix. It lives in java.util and solves a problem every Java developer has faced: how do you join a list of strings with commas (or any delimiter) without an ugly trailing comma at the end?

Before Java 8, building a delimited string required manual StringBuilder logic — appending elements, tracking whether to add a delimiter, and trimming the last character. StringJoiner handles all of this automatically.

The Problem StringJoiner Solves

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

public class StringJoinerIntro {
    public static void main(String[] args) {
        List<String> fruits = List.of("Apple", "Banana", "Cherry");

        // BEFORE Java 8: manual StringBuilder with trailing delimiter issue
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < fruits.size(); i++) {
            sb.append(fruits.get(i));
            if (i < fruits.size() - 1) {
                sb.append(", ");
            }
        }
        System.out.println(sb.toString());
        // Output: Apple, Banana, Cherry

        // WITH StringJoiner: clean and simple
        StringJoiner joiner = new StringJoiner(", ");
        for (String fruit : fruits) {
            joiner.add(fruit);
        }
        System.out.println(joiner.toString());
        // Output: Apple, Banana, Cherry
    }
}

2. Basic Usage

The simplest StringJoiner constructor takes just a delimiter. You add elements with add() and get the result with toString().

2.1 Constructor and add()

The StringJoiner(CharSequence delimiter) constructor creates a joiner with the given delimiter between elements. The add() method appends a new element. Elements are added in order, and the delimiter is automatically inserted between them.

import java.util.StringJoiner;

public class BasicStringJoiner {
    public static void main(String[] args) {
        // Comma delimiter
        StringJoiner csv = new StringJoiner(", ");
        csv.add("Alice");
        csv.add("Bob");
        csv.add("Charlie");
        System.out.println(csv.toString());
        // Output: Alice, Bob, Charlie

        // Pipe delimiter
        StringJoiner piped = new StringJoiner(" | ");
        piped.add("Red");
        piped.add("Green");
        piped.add("Blue");
        System.out.println(piped.toString());
        // Output: Red | Green | Blue

        // Newline delimiter
        StringJoiner lines = new StringJoiner("\n");
        lines.add("Line 1");
        lines.add("Line 2");
        lines.add("Line 3");
        System.out.println(lines.toString());
        // Output:
        // Line 1
        // Line 2
        // Line 3

        // Single element: no delimiter added
        StringJoiner single = new StringJoiner(", ");
        single.add("Only One");
        System.out.println(single.toString());
        // Output: Only One

        // Empty joiner: returns empty string
        StringJoiner empty = new StringJoiner(", ");
        System.out.println("Empty: '" + empty.toString() + "'");
        // Output: Empty: ''
    }
}

2.2 setEmptyValue()

By default, an empty StringJoiner (one with no elements added) returns an empty string for delimiter-only constructors, or just the prefix + suffix for the three-argument constructor. You can override this with setEmptyValue() to return a custom string when no elements are added.

import java.util.StringJoiner;

public class SetEmptyValueDemo {
    public static void main(String[] args) {
        // Without setEmptyValue: returns prefix + suffix (or empty string)
        StringJoiner joiner1 = new StringJoiner(", ", "[", "]");
        System.out.println("Default empty: " + joiner1.toString());
        // Output: Default empty: []

        // With setEmptyValue: returns custom value when empty
        StringJoiner joiner2 = new StringJoiner(", ", "[", "]");
        joiner2.setEmptyValue("No items found");
        System.out.println("Custom empty: " + joiner2.toString());
        // Output: Custom empty: No items found

        // Once an element is added, setEmptyValue is ignored
        joiner2.add("Apple");
        System.out.println("After add: " + joiner2.toString());
        // Output: After add: [Apple]

        // Useful for displaying "N/A" or "none" in reports
        StringJoiner tags = new StringJoiner(", ");
        tags.setEmptyValue("(no tags)");
        System.out.println("Tags: " + tags.toString());
        // Output: Tags: (no tags)
    }
}

3. StringJoiner with Prefix and Suffix

The three-argument constructor StringJoiner(delimiter, prefix, suffix) adds a prefix before the first element and a suffix after the last element. This is extremely useful for building structured output like JSON arrays, SQL IN clauses, and HTML lists.

import java.util.StringJoiner;

public class PrefixSuffixDemo {
    public static void main(String[] args) {
        // JSON-like array output
        StringJoiner json = new StringJoiner(", ", "[", "]");
        json.add("\"Apple\"");
        json.add("\"Banana\"");
        json.add("\"Cherry\"");
        System.out.println(json.toString());
        // Output: ["Apple", "Banana", "Cherry"]

        // SQL IN clause
        StringJoiner sql = new StringJoiner(", ", "WHERE status IN (", ")");
        sql.add("'ACTIVE'");
        sql.add("'PENDING'");
        sql.add("'APPROVED'");
        System.out.println(sql.toString());
        // Output: WHERE status IN ('ACTIVE', 'PENDING', 'APPROVED')

        // HTML unordered list
        StringJoiner html = new StringJoiner("</li>\n  <li>", "<ul>\n  <li>", "</li>\n</ul>");
        html.add("Home");
        html.add("About");
        html.add("Contact");
        System.out.println(html.toString());
        // Output:
        // <ul>
        //   <li>Home</li>
        //   <li>About</li>
        //   <li>Contact</li>
        // </ul>

        // Parenthesized list
        StringJoiner params = new StringJoiner(", ", "(", ")");
        params.add("String name");
        params.add("int age");
        params.add("boolean active");
        System.out.println("public void create" + params.toString());
        // Output: public void create(String name, int age, boolean active)
    }
}

4. Merging StringJoiners

The merge() method combines the content of another StringJoiner into the current one. The merged content is treated as a single element -- only the content (without the other joiner's prefix and suffix) is added, using the current joiner's delimiter.

import java.util.StringJoiner;

public class MergeDemo {
    public static void main(String[] args) {
        // Two separate joiners
        StringJoiner fruits = new StringJoiner(", ", "[", "]");
        fruits.add("Apple");
        fruits.add("Banana");

        StringJoiner veggies = new StringJoiner(", ", "[", "]");
        veggies.add("Carrot");
        veggies.add("Pea");

        System.out.println("Fruits:  " + fruits.toString());
        System.out.println("Veggies: " + veggies.toString());
        // Output:
        // Fruits:  [Apple, Banana]
        // Veggies: [Carrot, Pea]

        // Merge veggies INTO fruits
        // Note: veggies' prefix/suffix are dropped, content is added
        fruits.merge(veggies);
        System.out.println("Merged:  " + fruits.toString());
        // Output: Merged:  [Apple, Banana, Carrot, Pea]

        // Merging an empty joiner has no effect
        StringJoiner empty = new StringJoiner(", ");
        StringJoiner nonEmpty = new StringJoiner(", ");
        nonEmpty.add("Hello");
        nonEmpty.merge(empty);
        System.out.println("After merging empty: " + nonEmpty.toString());
        // Output: After merging empty: Hello

        // Practical: merge results from multiple data sources
        StringJoiner localUsers = new StringJoiner(", ");
        localUsers.add("Alice");
        localUsers.add("Bob");

        StringJoiner remoteUsers = new StringJoiner(", ");
        remoteUsers.add("Charlie");
        remoteUsers.add("Diana");

        StringJoiner allUsers = new StringJoiner(", ", "Users: [", "]");
        allUsers.merge(localUsers);
        allUsers.merge(remoteUsers);
        System.out.println(allUsers.toString());
        // Output: Users: [Alice, Bob, Charlie, Diana]
    }
}

5. String.join() Static Method

Java 8 also added the String.join() static method, which internally uses StringJoiner. It is the simplest way to join strings when you do not need a prefix, suffix, or incremental building.

String.join() has two overloads:

  • String.join(delimiter, elements...) -- join varargs
  • String.join(delimiter, Iterable) -- join any Iterable (List, Set, etc.)
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

public class StringJoinDemo {
    public static void main(String[] args) {
        // Join varargs
        String result1 = String.join(", ", "Alice", "Bob", "Charlie");
        System.out.println(result1);
        // Output: Alice, Bob, Charlie

        // Join a List
        List<String> languages = List.of("Java", "Python", "Go");
        String result2 = String.join(" | ", languages);
        System.out.println(result2);
        // Output: Java | Python | Go

        // Join a Set (order depends on Set implementation)
        Set<String> sorted = new TreeSet<>(Set.of("Banana", "Apple", "Cherry"));
        String result3 = String.join(" -> ", sorted);
        System.out.println(result3);
        // Output: Apple -> Banana -> Cherry

        // Building file paths
        String path = String.join("/", "home", "user", "documents", "report.txt");
        System.out.println(path);
        // Output: home/user/documents/report.txt

        // Building CSS class list
        String classes = String.join(" ", "btn", "btn-primary", "btn-lg", "active");
        System.out.println("class=\"" + classes + "\"");
        // Output: class="btn btn-primary btn-lg active"

        // Empty collection: returns empty string
        String empty = String.join(", ", List.of());
        System.out.println("Empty: '" + empty + "'");
        // Output: Empty: ''
    }
}

6. Collectors.joining()

When working with Streams, Collectors.joining() is the preferred way to join elements. It supports delimiter, prefix, and suffix -- the same features as StringJoiner -- but integrates directly into the Stream pipeline. Internally, it uses StringJoiner.

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

public class CollectorsJoiningDemo {
    public static void main(String[] args) {
        List<String> names = List.of("Alice", "Bob", "Charlie", "Diana");

        // Simple delimiter
        String csv = names.stream()
            .collect(Collectors.joining(", "));
        System.out.println(csv);
        // Output: Alice, Bob, Charlie, Diana

        // Delimiter + prefix + suffix
        String json = names.stream()
            .map(n -> "\"" + n + "\"")
            .collect(Collectors.joining(", ", "[", "]"));
        System.out.println(json);
        // Output: ["Alice", "Bob", "Charlie", "Diana"]

        // Practical: build SQL IN clause from a list of IDs
        List<Integer> ids = List.of(101, 205, 308, 412);
        String sqlIn = ids.stream()
            .map(String::valueOf)
            .collect(Collectors.joining(", ", "SELECT * FROM users WHERE id IN (", ")"));
        System.out.println(sqlIn);
        // Output: SELECT * FROM users WHERE id IN (101, 205, 308, 412)

        // Filter + transform + join
        List<String> emails = List.of(
            "alice@example.com", "bob@test.org",
            "admin@example.com", "diana@test.org"
        );
        String exampleDomainUsers = emails.stream()
            .filter(e -> e.endsWith("@example.com"))
            .map(e -> e.substring(0, e.indexOf("@")))
            .map(String::toUpperCase)
            .collect(Collectors.joining(", "));
        System.out.println("Example.com users: " + exampleDomainUsers);
        // Output: Example.com users: ALICE, ADMIN

        // No-arg joining(): concatenates without delimiter
        String together = names.stream()
            .collect(Collectors.joining());
        System.out.println(together);
        // Output: AliceBobCharlieDiana
    }
}

7. StringJoiner vs StringBuilder

StringJoiner and StringBuilder both build strings incrementally, but they serve different purposes. Here is when to use each.

Feature StringJoiner StringBuilder
Purpose Building delimited strings General string building
Delimiter handling Automatic Manual
Prefix/suffix Built-in Manual
Merge support Yes (merge()) No
Empty value Configurable Returns ""
Index access No Yes (charAt, insert, delete)
Flexibility Limited to delimited joining Full string manipulation
Stream support Via Collectors.joining() Manual
Best for CSV, SQL, JSON, log output Complex string assembly, templates

Rule of thumb: If you are joining elements with a delimiter, use StringJoiner (or String.join() / Collectors.joining()). If you need fine-grained control like inserting at specific positions, deleting characters, or building non-delimited strings, use StringBuilder.

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

public class JoinerVsBuilder {
    public static void main(String[] args) {
        List<String> items = List.of("Apple", "Banana", "Cherry");

        // StringBuilder approach: manual delimiter logic
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < items.size(); i++) {
            sb.append(items.get(i));
            if (i < items.size() - 1) {
                sb.append(", ");
            }
        }
        sb.append("]");
        System.out.println("StringBuilder: " + sb.toString());
        // Output: StringBuilder: [Apple, Banana, Cherry]

        // StringJoiner approach: automatic delimiter, prefix, suffix
        StringJoiner sj = new StringJoiner(", ", "[", "]");
        items.forEach(sj::add);
        System.out.println("StringJoiner:  " + sj.toString());
        // Output: StringJoiner:  [Apple, Banana, Cherry]

        // StringBuilder is better for non-delimited building
        StringBuilder html = new StringBuilder();
        html.append("<div class=\"card\">\n");
        html.append("  <h3>").append("Title").append("</h3>\n");
        html.append("  <p>").append("Description").append("</p>\n");
        html.append("</div>");
        System.out.println(html.toString());
    }
}

8. Practical Examples

These examples show StringJoiner solving real-world problems you will encounter in production Java code.

8.1 CSV Row Generation

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

public class CsvGenerator {
    public static void main(String[] args) {
        // Generate CSV with header and data rows
        StringJoiner csv = new StringJoiner("\n");

        // Header row
        csv.add(String.join(",", "Name", "Age", "City", "Email"));

        // Data rows
        String[][] data = {
            {"Alice", "30", "New York", "alice@example.com"},
            {"Bob", "25", "London", "bob@test.org"},
            {"Charlie", "35", "Tokyo", "charlie@mail.com"}
        };

        for (String[] row : data) {
            csv.add(String.join(",", row));
        }

        System.out.println(csv.toString());
        // Output:
        // Name,Age,City,Email
        // Alice,30,New York,alice@example.com
        // Bob,25,London,bob@test.org
        // Charlie,35,Tokyo,charlie@mail.com
    }
}

8.2 Building SQL Queries

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

public class SqlBuilder {
    public static void main(String[] args) {
        // INSERT statement
        List<String> columns = List.of("name", "email", "age", "city");
        List<String> values = List.of("'Alice'", "'alice@example.com'", "30", "'New York'");

        StringJoiner colJoiner = new StringJoiner(", ", "(", ")");
        columns.forEach(colJoiner::add);

        StringJoiner valJoiner = new StringJoiner(", ", "(", ")");
        values.forEach(valJoiner::add);

        String insert = "INSERT INTO users " + colJoiner + " VALUES " + valJoiner;
        System.out.println(insert);
        // Output: INSERT INTO users (name, email, age, city) VALUES ('Alice', 'alice@example.com', 30, 'New York')

        // WHERE clause with multiple conditions
        StringJoiner where = new StringJoiner(" AND ", "WHERE ", "");
        where.add("status = 'ACTIVE'");
        where.add("age >= 18");
        where.add("city IN ('New York', 'London')");
        System.out.println("SELECT * FROM users " + where);
        // Output: SELECT * FROM users WHERE status = 'ACTIVE' AND age >= 18 AND city IN ('New York', 'London')

        // UPDATE SET clause
        StringJoiner setClause = new StringJoiner(", ", "SET ", "");
        setClause.add("name = 'Bob'");
        setClause.add("email = 'bob@new.com'");
        setClause.add("updated_at = NOW()");
        System.out.println("UPDATE users " + setClause + " WHERE id = 1");
        // Output: UPDATE users SET name = 'Bob', email = 'bob@new.com', updated_at = NOW() WHERE id = 1
    }
}

8.3 Formatting Log Output

import java.time.LocalDateTime;
import java.util.StringJoiner;

public class LogFormatter {
    public static void main(String[] args) {
        // Structured log line: timestamp | level | class | message
        StringJoiner log1 = new StringJoiner(" | ");
        log1.add(LocalDateTime.now().toString());
        log1.add("INFO");
        log1.add("UserService");
        log1.add("User alice@example.com logged in");
        System.out.println(log1.toString());
        // Output: 2026-02-28T10:30:00.123 | INFO | UserService | User alice@example.com logged in

        // Key-value pairs for structured logging
        StringJoiner log2 = new StringJoiner(", ", "{", "}");
        log2.add("\"action\": \"login\"");
        log2.add("\"user\": \"alice\"");
        log2.add("\"ip\": \"192.168.1.1\"");
        log2.add("\"success\": true");
        System.out.println(log2.toString());
        // Output: {"action": "login", "user": "alice", "ip": "192.168.1.1", "success": true}
    }
}

9. Complete Practical Example

This example builds a report generator that can output data in three different formats -- CSV, JSON, and HTML -- all using StringJoiner. It demonstrates every concept covered in this tutorial.

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

class Employee {
    private final String name;
    private final String department;
    private final double salary;

    Employee(String name, String department, double salary) {
        this.name = name;
        this.department = department;
        this.salary = salary;
    }

    String getName() { return name; }
    String getDepartment() { return department; }
    double getSalary() { return salary; }
}

class ReportGenerator {

    // === CSV FORMAT ===
    String generateCsv(List<Employee> employees) {
        StringJoiner report = new StringJoiner("\n");
        report.setEmptyValue("No employee data available");

        // Header row using String.join()
        report.add(String.join(",", "Name", "Department", "Salary"));

        // Data rows
        for (Employee emp : employees) {
            StringJoiner row = new StringJoiner(",");
            row.add(emp.getName());
            row.add(emp.getDepartment());
            row.add(String.format("%.2f", emp.getSalary()));
            report.add(row.toString());
        }

        return report.toString();
    }

    // === JSON FORMAT ===
    String generateJson(List<Employee> employees) {
        // Each employee as a JSON object using StringJoiner
        String items = employees.stream()
            .map(emp -> {
                StringJoiner obj = new StringJoiner(", ", "    {", "}");
                obj.add("\"name\": \"" + emp.getName() + "\"");
                obj.add("\"department\": \"" + emp.getDepartment() + "\"");
                obj.add("\"salary\": " + emp.getSalary());
                return obj.toString();
            })
            .collect(Collectors.joining(",\n", "[\n", "\n]"));

        return items;
    }

    // === HTML TABLE FORMAT ===
    String generateHtml(List<Employee> employees) {
        StringJoiner table = new StringJoiner("\n");
        table.setEmptyValue("<p>No employees found.</p>");

        table.add("<table border=\"1\">");

        // Header using StringJoiner with prefix/suffix
        StringJoiner header = new StringJoiner("</th><th>", "  <tr><th>", "</th></tr>");
        header.add("Name");
        header.add("Department");
        header.add("Salary");
        table.add(header.toString());

        // Data rows
        for (Employee emp : employees) {
            StringJoiner row = new StringJoiner("</td><td>", "  <tr><td>", "</td></tr>");
            row.add(emp.getName());
            row.add(emp.getDepartment());
            row.add(String.format("$%.2f", emp.getSalary()));
            table.add(row.toString());
        }

        table.add("</table>");
        return table.toString();
    }

    // === SUMMARY using merge() ===
    String generateSummary(List<Employee> employees) {
        // Merge department groups
        StringJoiner engineering = new StringJoiner(", ");
        StringJoiner marketing = new StringJoiner(", ");
        StringJoiner other = new StringJoiner(", ");

        for (Employee emp : employees) {
            switch (emp.getDepartment()) {
                case "Engineering" -> engineering.add(emp.getName());
                case "Marketing" -> marketing.add(emp.getName());
                default -> other.add(emp.getName());
            }
        }

        StringJoiner summary = new StringJoiner("\n");
        summary.add("=== Department Summary ===");

        StringJoiner engLine = new StringJoiner(", ", "Engineering: ", "");
        engLine.merge(engineering);
        summary.add(engLine.toString());

        StringJoiner mktLine = new StringJoiner(", ", "Marketing: ", "");
        mktLine.merge(marketing);
        summary.add(mktLine.toString());

        // Total salary using Collectors.joining for labels
        String totalLine = "Total employees: " + employees.size();
        summary.add(totalLine);

        double totalSalary = employees.stream()
            .mapToDouble(Employee::getSalary)
            .sum();
        summary.add(String.format("Total salary: $%.2f", totalSalary));

        return summary.toString();
    }
}

public class ReportApp {
    public static void main(String[] args) {
        List<Employee> employees = List.of(
            new Employee("Alice", "Engineering", 95000),
            new Employee("Bob", "Marketing", 72000),
            new Employee("Charlie", "Engineering", 105000),
            new Employee("Diana", "Marketing", 68000),
            new Employee("Eve", "Engineering", 115000)
        );

        ReportGenerator generator = new ReportGenerator();

        // CSV Report
        System.out.println("=== CSV Report ===");
        System.out.println(generator.generateCsv(employees));

        System.out.println();

        // JSON Report
        System.out.println("=== JSON Report ===");
        System.out.println(generator.generateJson(employees));

        System.out.println();

        // HTML Report
        System.out.println("=== HTML Report ===");
        System.out.println(generator.generateHtml(employees));

        System.out.println();

        // Summary with merge()
        System.out.println(generator.generateSummary(employees));

        System.out.println();

        // Empty report: demonstrates setEmptyValue
        System.out.println("=== Empty CSV Report ===");
        System.out.println(generator.generateCsv(List.of()));

        System.out.println();
        System.out.println("=== Empty HTML Report ===");
        System.out.println(generator.generateHtml(List.of()));
    }
}
// Output:
// === CSV Report ===
// Name,Department,Salary
// Alice,Engineering,95000.00
// Bob,Marketing,72000.00
// Charlie,Engineering,105000.00
// Diana,Marketing,68000.00
// Eve,Engineering,115000.00
//
// === JSON Report ===
// [
//     {"name": "Alice", "department": "Engineering", "salary": 95000.0},
//     {"name": "Bob", "department": "Marketing", "salary": 72000.0},
//     {"name": "Charlie", "department": "Engineering", "salary": 105000.0},
//     {"name": "Diana", "department": "Marketing", "salary": 68000.0},
//     {"name": "Eve", "department": "Engineering", "salary": 115000.0}
// ]
//
// === HTML Report ===
// <table border="1">
//   <tr><th>Name</th><th>Department</th><th>Salary</th></tr>
//   <tr><td>Alice</td><td>Engineering</td><td>$95000.00</td></tr>
//   <tr><td>Bob</td><td>Marketing</td><td>$72000.00</td></tr>
//   <tr><td>Charlie</td><td>Engineering</td><td>$105000.00</td></tr>
//   <tr><td>Diana</td><td>Marketing</td><td>$68000.00</td></tr>
//   <tr><td>Eve</td><td>Engineering</td><td>$115000.00</td></tr>
// </table>
//
// === Department Summary ===
// Engineering: Alice, Charlie, Eve
// Marketing: Bob, Diana
// Total employees: 5
// Total salary: $455000.00
//
// === Empty CSV Report ===
// No employee data available
//
// === Empty HTML Report ===
// <p>No employees found.</p>

Concepts Demonstrated

Concept Where Used
StringJoiner(delimiter) CSV row builder, department grouping
StringJoiner(delimiter, prefix, suffix) JSON objects, HTML table rows, SQL-style output
add() Every joiner instance
merge() Department summary -- merging grouped joiners
setEmptyValue() CSV and HTML empty report fallback
String.join() CSV header row
Collectors.joining() JSON array with stream pipeline
Stream + StringJoiner JSON generation with map + collect

10. StringJoiner Quick Reference

Method / Class Description Example
new StringJoiner(delim) Create with delimiter only new StringJoiner(", ")
new StringJoiner(delim, prefix, suffix) Create with delimiter, prefix, suffix new StringJoiner(", ", "[", "]")
add(element) Append an element joiner.add("Alice")
merge(other) Merge another joiner's content joiner.merge(otherJoiner)
setEmptyValue(value) Custom string when no elements added joiner.setEmptyValue("N/A")
toString() Get the joined result joiner.toString()
length() Length of the current result joiner.length()
String.join(delim, elements...) Static join with varargs String.join(", ", "A", "B")
String.join(delim, iterable) Static join with Iterable String.join(", ", list)
Collectors.joining() Stream collector, no delimiter stream.collect(Collectors.joining())
Collectors.joining(delim) Stream collector with delimiter stream.collect(Collectors.joining(", "))
Collectors.joining(delim, pre, suf) Stream collector with all options stream.collect(Collectors.joining(", ", "[", "]"))