Java 21 brings in a lot of cool features, and one of them is the preview of String Templates*. While it serves more purposes than just classic String interpolation, for us Java developers, it’s yet another way to concatenate Strings in a “proper” way.
What is proper, though? I poked around the bytecode and learned some interesting and surprising things about different String concatenation and interpolation techniques in modern Java.
I’ve also compared it with the Kotlin way (under the hood) 😏.
But let’s begin with Java.
+
operator
We’ve always known that using a +
operator is bad practice since Strings are immutable, and under the hood,
a new String gets instantiated for every part we concatenate. However, as they say in Dutch, “meten is weten,”
which means “measuring is knowing.” Let’s see what is really happening inside:
In the bytecode, example #1 translates into a single String allocation. Java compiler is smart enough to see that the magic number is constant, so it is loaded as part of the String onto the operand stack:
Of course, we don’t want to use the magic values, so let’s see what happens with the variables.
In example #2, the Java compiler can’t perform the same optimisation when we’re using a variable,
but it does some advanced stuff with invokedynamic
:
This instruction allows bootstrapping the method at runtime that needs to be called for concatenation.
We’re giving it a recipe: some String \u0001 other String \u0001
, which in this case, contains two placeholders.
If we concatenate more variables, there will be more placeholders, but it will still be a single String in the
Constant Pool.
The cool thing about invokedynamic
approach is that when the newer JDK versions appear with newer concatenation
techniques, the bytecode can stay the same while the bootstrap method does something more advanced
(more on the current implementation a bit later).
What about example #3? In this case, the following instruction will be executed in a loop:
This will lead to unnecessary amounts of String instances being allocated.
String::format
I had a preconception that String::format
is a better alternative to the +
operator.
This method can indeed offer improved readability in some cases and supports localization.
Some basic benchmarking shows slightly better performance compared to concatenation. However,
implementing the format
method creates a new String for each parameter.
Let’s run a small experiment:
In the bytecode, we’re placing all the values on the operand stack and simply invoking a static method:
Now, let’s look at the heap dump taken after this method is invoked. For that, let’s compile the program and run it with the garbage collector disabled (so it doesn’t collect the String instances before we can take a look at them):
I am using VisualVM to create a heap dump. In the String instances section, I can see the following values:
Java newest String templates
The new String template feature is good, but not because it uses memory efficiently.
Actually, for basic cases, it behaves exactly the same as String concatenation under the hood.
It utilizes the invokedynamic
instruction passes the recipe over to the bootstrap method allowing it to do its magic.
The String templates are amazing because they can redefine the way of template processing and allow us to create other types besides String (if we want to). I enjoyed reading this article that tells more about it.
invokedynamic
approach
We figured out that invokedynamic
is used for most of the modern String concatenation/interpolation
techniques in Java.
Is it perfect? In terms of redundant String allocation, it’s not.
We’ve seen that we’re passing the recipe (template with placeholders) as a single String.
Now, if the values that have to be inserted into the placeholders are coming from the Constant Pool
(the \u0002
placeholder code), then there will be no extra Strings allocated.
On the other hand, if we’re using normal variables, the placeholder code will be \u0001
.
In this case, at runtime, the bootstrap method creates a separate String instance for every
piece between placeholders, and these Strings are combined with the parameters to construct a final String.
To see the proof of that, let’s consider this small example:
In the bytecode, we see the invokedynamic
with the single String which contains the recipe:
If we run the program with the garbage collector disabled and take the heap dump, we will see the following String instances (plus the resulting String, of course):
For comparison, if we used the StringBuilder
instead, it would look like this:
There would be only one “ and “
value allocated even if we type it in twice.
There will be three String instances: two fragments and the result.
What about Kotlin?
I apologize for the lack of fun in this section, but Kotlin (1.9.0) behaves similarly to Java under the hood.
The +
operator as well as the plus()
function and String interpolation syntax (for example,
val testStr = “this is $testNum test”
) all use invokedynamic
.
Some versions ago, both Java and Kotlin were using StringBuilder
internally to optimize String concatenation.
Now they use invokedynamic
, which allows to split the concatenation logic away from the bytecode
(it’s sitting in the bootstrap and target methods). The implementation will probably evolve, and other
JVM languages can benefit from it without making any changes (or with minor changes).
Conclusion
Regarding best practices, we probably don’t want to go against conventions. But we do want to know what’s happening inside.
What should we use? I only have the classic answer to this: it depends!
Perhaps, it’s not too bad to use regular +
operator sometimes? Maybe it does look more readable
in some cases (teeny tiny percent).
If we care about efficiency, we‘re better off using a StringBuilder
or a StringBuffer
.
StringBuffer
also gives all kinds of thread safety. String::format
seems to work somewhat fast,
but StringBuilder
is a lot faster. The downside of StringBuilder
is verbosity.
If we’re not too concerned about memory and speed but want to use powerful help in formatting and readability, String templates will be a great choice. Remember that they might become more efficient in newer versions and are much more than just a String interpolation mechanism.
Thank you for reading.
* The String Templates are a Preview feature in Java 21. This means that this feature is still being tested by the community; it’s not final and may change in the next versions. I’m not suggesting using preview features in production, but simply inviting you to analyse it alongside other concatenation techniques from the perspective of the internal workings.