Joining strings is one of the most used operation in any software development, not just in Java.
Collecting texts, generating reference numbers, completing a name or address, or those simple log message formatting – they all do string joining.
Let's see different ways to do it.
Concatenating Strings
Concatenate Operator
In Java, the string concatenate operator is the +
symbol. This is one of the most used method and one we always see in various Java tutorials.
int count = 100;
System.out.println("There are " + count + " transactions completed!");
println
call.The concept is very simple: Join the string at the left side of the +
symbol and the value at the right side. Just make sure that the value on the left side of +
symbol is a type of String
.
The drawback is that this operation is relatively expensive. Internally, joining two strings using the concatenate operator involves 3 strings. These are the 2 strings to be joined and the 3rd string created from the combined size of the first 2 strings – the 3rd string will contain the result of the concatenate operation. This is due to the way String
stores its value internally: using a byte[]
and instances of String
are immutable by contract, that is why String
s are safe to use and popular in various APIs. It's just the concatenation is really slow, especially on large texts.
String Builder Class
When there is a need to join a large number of strings, the StringBuilder
class is the best tool to use in this problem. This is done by using its .append()
method.
String largeString = new StringBuilder() // Creates a string builder
.append("this ")
.append("is ")
.append("an ")
.append("example ")
.append("large ")
.append("string!")
.toString(); // Produces the string
StringBuilder#append()
method used as a method chain to create a string.Instances of StringBuilder
is usually used to build strings that has usually unknown size and assumed to be very large.
StringBuilder accumulator = new StringBuilder();
Path inputFile = Path.of("sample-file.txt");
for (String line : Files.readAllLines(inputFile, StandardCharset.UTF_8)) {
accumulator.append(line);
accumulator.append("\n");
}
String fileContents = accumulator.toString();
System.out.println(fileContents);
StringBuilder
with unknown number of strings to be joined. Admittedly, this example is bad and impractical – this is purely for demonstration purposes.What made StringBuilder
better than the +
symbol when joining string is the way it stores its characters. Unlike String
's usages of a byte array, StringBuilder
works more like an ArrayList
– the storage grows or shrinks depending on the content stored.

+
symbol. Creating a string with 1.5M chars in 500k iterations took more than 40 seconds.
StringBuilder
is used. The operation completed in 15 milliseconds against the 40 seconds from above – that's around 2680× faster!Remember to use StringBuilder
when concatenating in a loop or any unknown-sized operations.

StringBuilder
in such cases.Joining Strings with Delimiter
Sometimes, we need to join strings with delimiters between the elements. Doing it using the methods above, +
symbol and the StringBuilder
class, you will require to implement additional logic on the beginning of the collection to properly delimit the strings:
List<String> fruits = List.of("apple", "lemon", "lychee");
StringBuilder joinedFruits = new StringBuilder();
for (String fruit : fruits) {
joinedFruits.append(fruit)
.append(", ");
}
System.out.println(joinedFruits); // Prints: apple, lemon, lychee,
", "
using loops.Notice the tailing comma after the word lychee
from the example above. This is not the intended result. The delimiter should not appear after the last element of the collection. This can be fixed by adding a special handling to either the beginning or the end of the string:
List<String> fruits = List.of("apple", "lemon", "lychee");
StringBuilder joinedFruits = new StringBuilder(fruits.get(0));
for (int i = 1; i < fruits.size(); i++) {
joinedFruits.append(", ")
.append(fruits.get(i));
}
System.out.println(joinedFruits); // Prints: apple, lemon, lychee
In this example, the tailing delimiter is now gone. However, the algorithm used seems unorthodox – we are used to do loops starting with element 0 and accumulating to an empty container. In this example, the loop starts at 1 and the accumulator has an initial value given to it.
Also notice that this method will break if the collection to join is empty. This problem can be easily fixed by adding a check before starting the join operation. As you can see, the algorithm is starting to go complex.
String .join()
Method
Starting from Java 8, a new static method .join()
is provided to easily create joined strings with delimiter:
List<String> fruits = List.of("apple", "lemon", "lychee");
String joinedFruits = String.join(", ", fruits);
System.out.println(joinedFruits); // Prints: apple, lemon, lychee
String.join()
.As you can see, the code is much simpler. There are no loops needed to implement. Just give the delimiter and the collection to join.
Collecting from Streams
It is possible to join strings that are extracted by mapping an object. There is a built-in collector in the Streams API that joins the strings with a given delimiter:
List<Fruit> fruits = fetchAllFruits(); // Assume that this method exists
String fruitNames = fruits.stream()
.map(Fruit::getName)
.collect(Collectors.joining(", "));
System.out.println(fruitNames); // Prints something like the previous example
Collectors.joining()
.This method is a little more complex, which is just fair since the data structure we are getting the string from is also more complex.
String Joiner Class
There is a specialized class that acts just like a StringBuilder
, but is specifically made for joining strings with delimiter: the StringJoiner
class.
String fruits = new StringJoiner(", ") // Delimeter is set in constructor
.add("apple") // Uses add() instead of append()
.add("lemon")
.add("lychee")
.toString(); // Also completes the string with toString()
StringBuilder
. Notice that StringJoiner
uses .add()
instead of .append()
.The StringJoiner
class is very useful when building multiple strings from a single source.
StringJoiner names = new StringJoiner(", ");
StringJoiner colors = new StringJoiner(", ");
List<Fruit> fruits = fetchAllFruits(); // Assume that this method exists
for (Fruit fruit : fruits) {
names.add(fruit.getName());
colors.add(fruit.getColor());
}
System.out.println(names);
System.out.println(colors);
names
and colors
. Using a single loop to build multiple strings is much performant than streaming the collection as many as the strings needed to be built.Conclusion
There are multiple ways to join strings. These multiple approaches to join strings exists to handle different cases.
The simplest way to join strings is to use the +
operator in strings. This is used in the simplest use cases of join – such as creation of log messages or exception message.
Another popular way to build strings is by using the StringBuilder
class. This approach is usually done when joining strings from a collection of unknown size. This approach is favored due to its performance.
Starting from Java 8, String.join()
method exists in the standard API to create delimited strings from a collection or arrays. This approach is favored when doing simple joins with delimiter.
When working with streams, the Collectors.joining()
is used in the .collect()
terminal method to join strings. This approach is favored when joining strings from objects.
Internally, String.join()
and Collectors.joining()
is using the StringJoiner
class. The StringJoiner
is not really used by itself but behind the scenes by using String.join()
or Collectors.joining()
via their respective APIs. Using StringJoiner
by itself means there is a special case that strings and streams cannot handle. Personally, I used it to create multiple strings from a single collection.