Use String.join (Java 8+): String.join(", ", list) turns List.of("Java", "Go", "Python") into Java, Go, Python. If the list holds numbers or other objects, stream it and convert each element first: list.stream().map(String::valueOf).collect(Collectors.joining(", ")).
Before Java 8 the JDK had no join method at all, which is why old answers loop over a StringBuilder or pull in Guava's Joiner or Apache Commons' StringUtils.join. Java 8 added three built-in tools: String.join for strings you already have, Collectors.joining for the end of a stream, and StringJoiner for building a result one piece at a time. This page covers all three, what they do with null elements, the trailing-separator bug in hand-written loops, the "a, b and c" variation, and String.repeat for repeating a string. Each example runs on this page: hit Run, then edit the code and run it again.
1String.joinRecommended
String.join(separator, elements) puts the separator between the elements, never after the last one, so there is nothing to trim. It has two overloads: one takes any Iterable of strings (a List, a Set, any collection), and one takes varargs, which also accepts a String[]. The elements must be strings (or another CharSequence); section 2 handles everything else.
Output
Prints Java, Go, Python, Java | Go | Python and JavaGoPython (an empty separator just concatenates), then apple, fig, pear in the TreeSet's sorted order, usr/local/bin and 2026-09-25. An empty list gives an empty string, which is why the brackets print as [], and a one-element list comes back as solo with no separator. String.join follows the collection's iteration order, so a HashSet or Set.of joins in an unspecified order (for Set.of it can change from run to run). Use a TreeSet, a LinkedHashSet or a sorted list when the order matters. The reverse operation is splitting a string.
2Numbers and objects: Collectors.joining
String.join won't accept a List<Integer>: the call doesn't compile. Stream the list instead, turn each element into a string with map(String::valueOf), and finish with Collectors.joining. It comes in three forms: no arguments (plain concatenation), a separator, or a separator plus a prefix and suffix that wrap the whole result. Because it is the last step of a stream, you can filter, sort and format the elements in the same expression.
Output
Prints 3, 1, 4, 1, 5, then [3, 1, 4, 1, 5] with the prefix and suffix, then #3 #4 #5 (the values above 1, sorted and formatted), then Point[x=1, y=2]; Point[x=3, y=4] from the record's generated toString(), and finally {}: an empty stream still gets its prefix and suffix. String::valueOf and Objects::toString do the same job here; both call toString() on the element and both turn a null into the text null. For an int[], start with Arrays.stream(arr).mapToObj(String::valueOf), as shown in print an array.
3Null elements: joined as "null", or filtered out
Neither String.join nor Collectors.joining skips nulls: each null element is written as the four characters null, which is rarely what you want in user-facing text. Filter them out with filter(Objects::nonNull), or swap in a default with Objects.toString(value, default). Empty and blank strings are a separate case: they aren't null, so both filters let them through.
Output
The first two lines both print Ada, null, Grace, null. Filtering gives Ada, Grace and the default gives Ada, ?, Grace, ?. The tag list prints java,streams: without the isBlank() check (Java 11+), the empty string and the spaces would leave stray commas. The last line, Ada, Grace, filters without a stream; removeIf needs a modifiable list, hence the ArrayList copy (see removing items from a list). A null list or a null separator is different: both throw a NullPointerException.
4StringJoiner and setEmptyValue
StringJoiner (Java 8+) is the tool for the cases where you add items one at a time: inside a loop, behind an if, or across several methods. You give it the separator and an optional prefix and suffix, then call add(). Its one feature the other two don't have is setEmptyValue, the text to return when nothing was added.
Output
Prints [Java, Python, Rust] (Go fails the length check) and 20, the length including the brackets. The empty joiner prints its bare [], then (none) once the empty value is set, then [first] after one add. The chained joiner prints 2026-09-25. The last [] comes from the brackets in the println, not a prefix: after add("") the joiner holds one empty element, so it returns an empty string instead of (none). Call toString() when you need a String rather than printing the joiner.
5A StringBuilder loop and the trailing-separator bug
A hand-written loop works on every Java version and is what most pre-Java 8 answers show. The classic mistake is appending the separator after every item, which leaves one dangling at the end. There are three standard fixes. Use a StringBuilder, not String +=: strings are immutable, so each += in a loop copies everything built so far, and the total work grows with the square of the output length.
Output
The buggy loop prints [Java, Go, Python, ]; the brackets make the trailing , visible. All three fixes print [Java, Go, Python], and the last line prints true: the result is identical to String.join. The guard in fix 3 matters, because on an empty builder setLength(-2) throws StringIndexOutOfBoundsException: String index out of range: -2. A common shortcut, if (sb.length() > 0) sb.append(", "), breaks when the first item is an empty string. On Java 8 and later, a StringJoiner gives you the same loop with none of these cases to get wrong.
6Variations: "a, b and c", and repeating a string
Text for people usually wants Java, Go and Python rather than Java, Go, Python. Join every item except the last with a comma, then attach the last one with and; one or two items only need " and ". The related question of repeating a string has had a one-call answer since Java 11: "ab".repeat(3). To repeat with a separator, join Collections.nCopies.
Output
Prints Java, Java and Go, Java, Go and Python, and Java, Go, and Python with the Oxford comma. subList is a view, so nothing is copied before the join. Then ababab, a line of 20 dashes, and []: a count of 0 returns an empty string, while a negative count throws IllegalArgumentException: count is negative: -1. The last line, ?, ?, ?, is the usual way to build the placeholders for a SQL IN (...) clause.
7Which should you use?
| Method | Elements | Needs | Best for |
|---|---|---|---|
| String.join(", ", list) | Strings only | Java 8+ | A list, set or array of strings: the default |
| stream().map(String::valueOf).collect(joining(", ")) | Anything | Java 8+ | Numbers and objects; filtering or sorting first |
| Collectors.joining(sep, prefix, suffix) | Strings (map first) | Java 8+ | Wrapping the result in brackets or braces |
| new StringJoiner(sep, prefix, suffix) | Strings, one add() at a time | Java 8+ | Loops with conditions; a custom empty value |
| StringBuilder loop | Anything | Any version | Pre-Java 8 code; watch the trailing separator |
| String += in a loop | Anything | Quadratic copying | Nothing: use one of the above |
| "ab".repeat(n) | One string | Java 11+ | Repeating a string n times |
Frequently asked questions
How do I convert a List<String> to a comma-separated String in Java?
Call String.join(", ", list) (Java 8+). It puts the separator between the elements only, so there is no trailing comma, an empty list gives an empty string, and a one-element list comes back unchanged. For a different layout, change the first argument, for example String.join("\n", list) for one item per line. Before Java 8 the options were a StringBuilder loop, Guava’s Joiner or Apache Commons’ StringUtils.join.
Why does String.join not compile with a List<Integer>?
String.join only accepts an Iterable<? extends CharSequence> or CharSequence varargs, and Integer is neither, so javac reports no suitable method found for join(java.lang.String,java.util.List<java.lang.Integer>). Convert the elements in a stream: nums.stream().map(String::valueOf).collect(Collectors.joining(", ")) turns List.of(3, 1, 4, 1, 5) into 3, 1, 4, 1, 5.
What happens to null elements when joining a list in Java?
Both String.join and Collectors.joining write a null element as the text null, so Arrays.asList("Ada", null, "Grace", null) joins to Ada, null, Grace, null. Remove them with .filter(Objects::nonNull) before collect, or replace them with .map(s -> Objects.toString(s, "?")). A null list or a null separator is an error: String.join throws a NullPointerException.
What is the difference between String.join, Collectors.joining and StringJoiner?
They produce the same result and differ in where the elements come from. String.join takes strings you already have in a collection or array. Collectors.joining is the last step of a stream, so it pairs with map, filter and sorted, and it can add a prefix and suffix. StringJoiner lets you add() one element at a time and set a custom value for the empty case with setEmptyValue.
How do I repeat a string in Java?
On Java 11+, call "ab".repeat(3), which returns ababab. A count of 0 returns an empty string and a negative count throws IllegalArgumentException: count is negative: -1. On Java 8, String.join("", Collections.nCopies(3, "ab")) gives the same ababab, and swapping the empty string for ", " repeats with a separator. Guava’s Strings.repeat and Commons’ StringUtils.repeat are the library versions.
How do I join a Map into key=value pairs?
Stream its entries and format each one: map.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&")). The order follows the map, so use a TreeMap or LinkedHashMap when it matters; HashMap order is unspecified and Map.of order can change from run to run. A TreeMap of port=8080, host=1 and debug=0 joins to debug=0&host=1&port=8080.
Run it yourself
Open any of these in the full Java editor to tweak, run and share them.
Java playground