SoatDev IT Consulting
SoatDev IT Consulting
  • About us
  • Expertise
  • Services
  • How it works
  • Contact Us
  • News
  • August 18, 2023
  • Rss Fetcher
Image by author

Java 21 brings in a lot of cool features, and one of them is the 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:

// example #1:
String example1 = "some String " + 42;

// example #2:
int someInt = 42;
String example2 = "some String " + someInt + " other String " + someInt;

// example #3:
String example3 = "";
for (int i = 0; i < 10; i++) {
example3 += someInt;
}

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:

0: ldc     #7  // String some String42

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:

0: bipush        42
2: istore_1
3: iload_1
4: iload_1
5: invokedynamic #7, 0 // InvokeDynamic #0:makeConcatWithConstants:(II)Ljava/lang/String;

...

BootstrapMethods:
0: #22 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
Method arguments:
#23 some String u0001 other String u0001

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:

16: invokedynamic #9,  0  // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;

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:

int firstValue = 12345;
int secondValue = 987654321;
int thirdValue = 117117;
String test = String.format("test %s and %s and %s", firstValue, secondValue, thirdValue);

In the bytecode, we’re placing all the values on the operand stack and simply invoking a static method:

34: invokestatic  #15  // Method java/lang/String.format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;

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):

javac --enable-preview --source=21 Main.java
java --enable-preview -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Main

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:

int firstValue = 12345;
int secondValue = 987654321;
int thirdValue = 117117;

// alright, we can use the fancy string templates:
String test = STR."test {firstValue} and {secondValue} and {thirdValue}";

// but this line would result in exactly identical bytecode:
// String test = "test " + firstValue + " and " + secondValue + " and " + thirdValue;

In the bytecode, we see the invokedynamic with the single String which contains the recipe:

13: invokedynamic #9,  0  // InvokeDynamic #0:makeConcatWithConstants:(III)Ljava/lang/String;

...

BootstrapMethods:
0: #27 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
Method arguments:
#25 test u0001 and u0001 and u0001

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:

String test = new StringBuilder()
.append("test ")
.append(firstValue)
.append(" and ")
.append(secondValue)
.append(" and ")
.append(thirdValue)
.toString();

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.


Java 21: So How Should We Construct Strings Now? was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.

Previous Post
Next Post

Recent Posts

  • Tesla sends driverless Model Y from factory to customer to promote its robotaxi tech
  • Next-gen procurement platform Levelpath nabs $55M
  • Fintech Bolt progresses its turnaround by landing Klarna as a partner
  • Instagram now lets you share Spotify songs with sound to Stories
  • At TechCrunch All Stage: VC red flags, founder signals, and pre-seed traps — Charles Hudson will tell you what investors really see

Categories

  • Industry News
  • Programming
  • RSS Fetched Articles
  • Uncategorized

Archives

  • June 2025
  • May 2025
  • April 2025
  • February 2025
  • January 2025
  • December 2024
  • November 2024
  • October 2024
  • September 2024
  • August 2024
  • July 2024
  • June 2024
  • May 2024
  • April 2024
  • March 2024
  • February 2024
  • January 2024
  • December 2023
  • November 2023
  • October 2023
  • September 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • April 2023

Tap into the power of Microservices, MVC Architecture, Cloud, Containers, UML, and Scrum methodologies to bolster your project planning, execution, and application development processes.

Solutions

  • IT Consultation
  • Agile Transformation
  • Software Development
  • DevOps & CI/CD

Regions Covered

  • Montreal
  • New York
  • Paris
  • Mauritius
  • Abidjan
  • Dakar

Subscribe to Newsletter

Join our monthly newsletter subscribers to get the latest news and insights.

© Copyright 2023. All Rights Reserved by Soatdev IT Consulting Inc.