Back to Blog
Java

Java String Join: Methods and Examples

java string join: Learn how to join strings in Java using String.join, Collectors.joining, StringJoiner, and StringBuilder, with practical examples and tradeoffs.

String.joinCollectors.joiningStringJoinerStringBuilderJava Streams
Diagram comparing Java string join methods with delimiter and list elements.

In Java, joining strings with a delimiter is a common operation. The java string join methods available in the standard library have evolved over time, and choosing the right one depends on your data source and requirements. This article covers the primary ways to join strings in Java, from the simple String.join to stream-based collectors and manual accumulation.

The Basics of String.join

String.join was introduced in Java 8 and is the simplest way to join a sequence of strings with a delimiter. It accepts a delimiter and either an array of CharSequence or an Iterable that extends CharSequence. Here is a basic example:

String joined = String.join(", ", "apple", "banana", "cherry"); System.out.println(joined); // apple, banana, cherry

The method also works with lists and sets:

List<String> fruits = Arrays.asList("apple", "banana", "cherry"); String result = String.join("; ", fruits);

The delimiter is placed between each pair of elements, but not after the last one. If the input collection is empty, the result is an empty string. If it contains a single element, the delimiter is not used at all.

Joining Collections and Iterables

When you have a collection that does not directly implement CharSequence, you cannot pass it directly to String.join. For example, a List<Integer> requires conversion to strings first. The straightforward approach is to map each element to a string and then collect:

List<Integer> numbers = Arrays.asList(1, 2, 3); String result = numbers.stream() .map(String::valueOf) .collect(Collectors.joining(", "));

This uses the Collectors.joining collector, which is the idiomatic way to join stream elements. It is also the only way to join a stream of non-string objects without building an intermediate list.

Using Collectors.joining for Streams

Collectors.joining provides three overloads: one with just a delimiter, one with a delimiter and a prefix, and one with a delimiter, prefix, and suffix. The prefix and suffix are useful for producing formatted output such as a bracketed list:

String bracketed = numbers.stream() .map(String::valueOf) .collect(Collectors.joining(", ", "[", "]")); // [1, 2, 3]

The collector works on a Stream<CharSequence>, so you must map non-string types to strings first. It also handles empty streams by returning an empty string (or just the prefix and suffix if those are provided). This makes it a reliable choice for building CSV-like output or log messages.

StringJoiner for Custom Prefix and Suffix

The StringJoiner class, also introduced in Java 8, is the underlying implementation used by Collectors.joining. You can use it directly when you need to add elements incrementally rather than from a stream. For example:

StringJoiner joiner = new StringJoiner(", ", "{ ", " }"); joiner.add("one"); joiner.add("two"); System.out.println(joiner.toString()); // { one, two }

StringJoiner is mutable and not thread-safe, but it is useful when you are building a string across multiple method calls or loops. It also allows you to set an empty value, which is returned when no elements have been added:

StringJoiner emptyJoiner = new StringJoiner(", "); emptyJoiner.setEmptyValue("no values"); System.out.println(emptyJoiner.toString()); // no values

This can help avoid awkward conditional logic when the result may be empty.

Manual Joining with StringBuilder

Before Java 8, the common approach was to use StringBuilder in a loop. This is still valid and sometimes necessary when you need full control over formatting or when you are working with a custom iteration pattern. A typical implementation looks like this:

StringBuilder sb = new StringBuilder(); for (int i = 0; i < items.size(); i++) { if (i > 0) { sb.append(", "); } sb.append(items.get(i)); } String result = sb.toString();

The explicit if check prevents a trailing delimiter. This approach is more verbose, but it gives you the ability to conditionally skip elements or apply custom formatting per element. It also works with any object type because append calls String.valueOf internally.

Handling Nulls and Empty Elements

The behavior of String.join and Collectors.joining with null elements is a common source of confusion. String.join will throw a NullPointerException if any element is null, because it internally calls toString() on each element. The same is true for Collectors.joining. If your data may contain nulls, you need to filter or map them explicitly:

List<String> values = Arrays.asList("a", null, "c"); String result = values.stream() .filter(Objects::nonNull) .collect(Collectors.joining(", "));

Alternatively, you can map nulls to a placeholder:

String result = values.stream() .map(v -> v == null ? "N/A" : v) .collect(Collectors.joining(", "));

Empty strings are treated as normal elements, so String.join(", ", "a", "", "b") produces "a, , b". If you want to skip empty strings, you must filter them before joining.

Performance and Allocation Considerations

The standard joining methods are implemented with a StringJoiner internally, which uses a StringBuilder to accumulate the result. This avoids repeated string concatenation, which would create many intermediate String objects and hurt performance. The main cost is the initial allocation of the StringBuilder buffer, which grows as needed. For most use cases, the difference between String.join, Collectors.joining, and a manual StringBuilder loop is negligible. The choice should be driven by readability and the shape of your data.

One subtle performance detail is that Collectors.joining is a terminal operation on a stream. If you are joining a large collection, the stream overhead is usually small compared to the actual string building. However, if you are joining a simple list and do not need stream operations like filtering or mapping, String.join is more direct and slightly faster because it avoids the stream machinery.

Choosing the Right Joining Approach

The decision between these methods depends on your input and formatting requirements. Use String.join when you have an array or an Iterable of strings and need a simple delimiter. Use Collectors.joining when you are already working with a stream and need to map elements to strings or apply stream operations. Use StringJoiner when you need to add elements incrementally or require a custom empty value. Use a manual StringBuilder loop when you need conditional logic that the standard methods cannot express, such as skipping elements based on runtime state.

All of these approaches are compatible with Java 8 and later, so you do not need to worry about version-specific behavior as long as you are on a modern JDK. The key is to choose the method that makes your intent clearest, because the performance differences are rarely significant in real applications.

java string join: Practical Usage and Code Examples | RYUSLOG DEV