StringJoiner

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 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("\n  
  • ", "
      \n
    • ", "
    • \n
    "); html.add("Home"); html.add("About"); html.add("Contact"); System.out.println(html.toString()); // Output: //
      //
    • Home
    • //
    • About
    • //
    • Contact
    • //
    // 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 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 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 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 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 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 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("
    \n"); html.append("

    ").append("Title").append("

    \n"); html.append("

    ").append("Description").append("

    \n"); html.append("
    "); 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 columns = List.of("name", "email", "age", "city");
            List 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 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 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 employees) {
            StringJoiner table = new StringJoiner("\n");
            table.setEmptyValue("

    No employees found.

    "); table.add(""); // Header using StringJoiner with prefix/suffix StringJoiner header = new StringJoiner(""); header.add("Name"); header.add("Department"); header.add("Salary"); table.add(header.toString()); // 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())); table.add(row.toString()); } table.add("
    ", "
    ", "
    ", "
    ", "
    "); return table.toString(); } // === SUMMARY using merge() === String generateSummary(List 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 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 === // // // // // // // //
    NameDepartmentSalary
    AliceEngineering$95000.00
    BobMarketing$72000.00
    CharlieEngineering$105000.00
    DianaMarketing$68000.00
    EveEngineering$115000.00
    // // === 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 === //

    No employees found.

    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(", ", "[", "]"))



    Subscribe To Our Newsletter
    You will receive our latest post and tutorial.
    Thank you for subscribing!

    required
    required


    Leave a Reply

    Your email address will not be published. Required fields are marked *