1. Overview
In Java, string concatenation is a common operation when working with text manipulation. However, the way you choose to concatenate strings can have a significant impact on your application’s performance. It’s crucial to understand the different concatenation methods available and their performance characteristics to write efficient and optimized code.
In this tutorial, we’ll dive into different string concatenation methods in Java. We’ll benchmark and compare the execution times of these methods using tools JHM.
2. Benchmarking
We’ll adopt JMH (Java Microbenchmark Harness) for our benchmarking. JMH provides a framework for measuring the performance of small code snippets, enabling developers to analyze and compare different implementations.
Before we proceed, let’s set up our environment to run the benchmarks. Both Core and Annotation Processors can be found in Maven Central.
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.36</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.36</version>
</dependency>
3. Immutable String Concatenation
Immutable string concatenation involves creating a new immutable String instance for each concatenation operation. Every time a concatenation occurs, a new string object is generated. This method is simple and straightforward but can be less memory efficient due to the creation of multiple objects.
Now, let’s take a quick look at various immutable methods:
3.1. Using Addition (+) Operator
This is the simplest way and probably the one we’re most familiar with. It can concatenate string literals, variables, or a combination of both using addition + operator:
String str1 = "String";
String str2 = "Concat";
String result = str1 + str2;
3.2. Using concat() Method
The concat() method is provided by the String class and can be used to concatenate two strings together:
String str1 = "String";
String str2 = "Concat";
String result = str1.concat(str2);
3.3. Using String.join() Method
The String.join() is a new static method from Java 8 onward. It allows concatenating multiple strings using a specified delimiter:
String str1 = "String";
String str2 = "Concat";
String result = String.join("", str1, str2);
String.format() is used for formatting strings with placeholders and format specifiers. It allows you to create formatted strings by replacing placeholders with actual values:
String str1 = "String";
String str2 = "Concat";
String result = String.format("%s%s", str1, str2);
3.5. Using Java Stream API
Finally, we’ll take a look at Java Stream API, which is also available from Java 8. It provides an expressive way to perform operations on collections of object and allow us to concentrate strings using Collectors.joining():
List<String> strList = List.of("String", "Concat");
String result = strList.stream().collect(Collectors.joining());
4. Mutable String Concatenation
Now let’s shift our focus to the mutable category. This refers to the process of concatenating strings using a mutable character sequence, where the underlying object can be modified to append or insert characters. Mutable concatenation is efficient and doesn’t require creating new objects for each operation.
Let’s have a look at the available mutable methods:
4.1. Using StringBuffer
StringBuffer provides a mutable sequence of characters. It allows for dynamic manipulation of strings without creating new objects. It’s worth mentioning that it’s designed to be thread-safe, meaning it can be safely accessed and modified by multiple threads concurrently:
StringBuffer buffer = new StringBuffer();
buffer.add("String");
buffer.add('Concat");
String result = buffer.toString();
4.2. Using StringBuilder
StringBuilder serves the same purpose as StringBuffer. The only difference between them is StringBuilder isn’t thread-safe, while StringBuffer is. It’s perfect for single-threaded scenarios where thread safety is not a concern:
StringBuilder builder = new StringBuilder();
builder.add("String");
builder.add('Concat");
String result = builder.toString();
4.3. Using StringJoiner
StringJoiner is a new class from Java 8 onward. Its function is similar to StringBuilder, providing a way to join multiple strings with a delimiter. While it is the same as StringBuilder, StringJoiner isn’t thread-safe:
StringJoiner joiner = new StringJoiner("");
joiner.add("String");
joiner.add('Concat");
String result = joiner.toString();
In this section, we’ll evaluate the performance of the different string concatenation methods in various scenarios, including loop iterations and batch processing.
5.1. Loop Iteration
We’ll assess string concatenation performance within a loop, where strings are repeatedly concatenated. In this scenario, we’ll evaluate the performance of different methods with different numbers of iterations.
We’ll run tests with different iterations (100, 1000, and 10000) to see how computation time scales with the number of iterations. Let’s start with immutable methods:
|
Number of Iterations |
Methods |
100 |
1000 |
10000 |
+ Operator |
3.369 |
322.492 |
31274.622 |
concat() |
3.479 |
332.964 |
32526.987 |
String.join() |
4.809 |
331.807 |
31090.466 |
String.format() |
19.831 |
1368.867 |
121656.634 |
Stream API |
10.253 |
379.570 |
30803.985 |
Now, we can see the performance of mutable methods:
|
Number of Iterations |
Methods |
100 |
100 |
10000 |
StringBuffer |
1.326 |
13.080 |
128.746 |
StringBuilder |
0.512 |
4.599 |
43.306 |
StringJoiner |
0.569 |
5.873 |
59.713 |
From the figures above, we can observe distinct behavior between these categories regarding computation time increase with the number of iterations.
The computation time increases linearly with the data size in the mutable category. Whereas in the immutable category, the computation time increases exponentially. A ten-fold increase in concatenation operations results in a hundred-fold increase in computation time.
We can also observe that most methods in the mutable category exhibit similar computation times, except for String.format(). It is notably slower that taking a few times longer than other methods in the same category. The significant performance difference can be attributed to the additional parsing and replacement operations performed by String.format().
Among the immutable category, StringBuilder is the fastest option due to its lack of synchronization overhead compared to StringBuffer. On the other hand, StringJoiner exhibits a slightly slower performance than StringBuilderbecause it needs to append the delimiter each time it concatenates.
5.2. Batch Processing
Let’s go through some methods that allow concatenating more than 2 strings in one go in the mutable category. The example below illustrates a single String.join() with 5 concatenations:
String result = String.join("", str1, str2, str3, str4, str5);
We’ll access the performance of these methods in this section. Similar to the previous section, we’ll run tests with different numbers of concatenations (100, 1000, and 10000) in a batch:
|
Number of Concatenation |
Methods |
100 |
1000 |
10000 |
+ Operator |
0.777 |
33.768 |
StackOverflowError |
String.join() |
0.820 |
8.410 |
88.888 |
String.format() |
3.871 |
38.659 |
381.652 |
Stream API |
2.019 |
18.537 |
193.709 |
When we concatenate strings in batch, we observed linear computation time growth. Again, String.format() comes last again which is expected due to the additional parsing overhead.
6. Conclusion
In this article, we’ve explored different string concatenation methods in Java and evaluated their performance using JMH.
We observed distinct behaviour between mutable and immutable categories through performance evaluation in loop iteration. Computation time in the mutable category increased linearly with data size, while the immutable category showed exponential growth. Due to the scale pattern, we should adopt mutable methods whenever we concatenate strings within a loop.
Among all methods that we presented overall, StringBuilder stands out as the fastest one, whereas String.format() is the slowest one due to its additional parsing and replacement operations.
As usual, all the source code is available over on GitHub.