Bits of Knowledge: Joining Strings in Java

Bits of Knowledge: Joining Strings in Java

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!");
Simple message formatting inside the 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 Strings 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);
This is an example usage of 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.

large-string-using-plus
Sample large string creation using the + symbol. Creating a string with 1.5M chars in 500k iterations took more than 40 seconds.
large-string-using-stringbuilder
The same operation as above but this time 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.

intellij-concat-warning
Even IntelliJ IDEA suggests to use 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,
Simple joining with delimiter ", " 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
Joining that seeds the accumulator with the first element of the string, and loops starting from the 2nd element.

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
Creation of a string using 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
Creation of string from a list of objects using 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()
The code almost looks like a 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);
Created 2 strings 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.

Show Comments