Looking at Java 21: String Templates
Java’s String type is a ubiquitous type that’s used in every program out there. It’s so omnipresent, we often don’t think much about it, and take it as a given. However, over the years, it received many improvements, like better optimization possibilities and multi-line blocks. And now, another exciting improvement is coming that makes String safer and easier to use: String templates.
Table of Contents
How to compose Strings in Java
To better understand and evaluate String templates, let’s first look at how we can compose String without them.
So far, we have several mechanisms and types that work with String literals and instances built right into the language/JDK to that:
- The
+
(plus) operator StringBuffer
andStringBuilder
String::format
andString::formatted
java.text.MessageFormat
Each of them has use cases, but also particular downsides.
The + (plus) Operator
The operator is built right into the language to concat String literals or variables:
It’s easy to use, we can even throw non-String values into the mix.
However, the resulting code isn’t really pretty or fun to write.
And the biggest downside is that a new String gets allocated each time we use the +
operator.
In our case here, that means 5 String get allocated, which might not seem much, but how about using the operator in a loop?
Behind the scenes, the JVM has multiple optimization strategies to reduce String allocation, like replacing the operator with a StringBuilder
or using invokedynamic
.
Even though these optimizations are quite nice, it’s still better to not rely solely on possible optimizations and choose a more appropriate approach in the first place.
StringBuffer and StringBuilder
The two types java.lang.StringBuffer
and java.lang.StringBuilder
are special tools built for String concatenation, plus they also have additional methods for inserting, replacing, and finding a String.
StringBuffer
is thread-safe and available since Java’s inception, whereas StringBuilder
got added in Java 5 as an “API compatible more performant but not thread-safe” alternative.
Their major downside is their verbosity, especially for simpler Strings:
Although StringBuilder
offers great performance, that’s why it’s used by the JVM for optimizing the +
operator, we shouldn’t replace all String manipulation with a StringBuilder
automatically.
Performance characteristics are fickle beasts and depend on the size of a String, the kind of manipulation we’re doing, hardware constraints, etc.
If we might have performance issues and are in doubt about what to do, benchmark it, to verify that it offers a significant performance improvement.
String::format and String::formatted
The String
type has three methods for formatting:
static String format(String format, Object... args)
static String format(Locale locale, String format, Object... args)
String formatted(Object... args)
(Java 15+)
They allow for reusable templates, but they require format specifiers and provide the variables in the correct order:
As you can imagine, using format specifiers requires creating a Formatter
for the template String.
Even though you save on the number of String allocations, now the JVM has to parse/validate the template String.
java.text.MessageFormat
The java.text.MessageFormat
type is like the older sibling of String::format
, as it uses the same approach of using format String container specifiers.
However, it’s more verbose and its syntax is unfamiliar to many devs these days.
The following example is the most simplistic variant, without additional formattings, like leading zeros:
It shares the same downsides with String::format
.
However, it has a few additional tricks up its sleeves, like handling plurals.
String Interpolation to the Rescue
The discussed techniques for composing Strings so far were all about concatenation. However, many languages, like Groovy or Swift, also support interpolation directly in String literals by wrapping variables or expressions into special constructs:
Especially paired with multi-line Strings, interpolation trumps any concatenation approach in readability and simplicity:
Seems like a simple feature and should be easy enough to add to a language.
Just define which syntax the wrapper should have, like ${}
(Groovy) or \()
(Swift), and add it to String
literal parsing.
However, there’s a big downside to such a simplistic approach to interpolation.
The Dangers of String Interpolation
Most languages implement String interpolation in the following way:
- Evaluate expression/variable
- Convert to String value if needed
- Insert String representation into the original String literal
Don’t get me wrong, this approach is already immensely helpful, but it has a major drawback… what if replacing the result of the interpolation would create an invalid overall String literal? This is especially dangerous if the result is used without previous validation or correct escaping of values.
That’s why the designers of Java asked themselves if they can do better than just adding String interpolation.
Java String Templates
One critique I often hear about Java is its verbosity and lack of certain “simple” features or making things more complicated than it needs to be. On the surface, there’s some truth to it. But if you dig just a little deeper, you will realize that there are good reasons why some features take quite some time to be added to the language, or features aren’t as “simple or concise” compared to other languages.
The main reason behind this is that Java’s language designers are perfectly willing to forgo a certain degree of functionality or convenience if that means a feature is safer to use but still useful, or even provides more usability than a simpler default approach.
In the case of String templates, their goal was to provide the clarity of interpolation with safer out-of-the-box result, plus the options to extend and bend the feature to our needs if required. The result I want to show you here might be different from other languages’ simple String interpolation, but in return, it gives us a more flexible and versatile scaffold.
Template Expressions
The new way to work with Strings in Java is called template expression, a programmable way of safely interpolating expressions in String literals. And even better than just interpolating, we can turn structured text into any object, not just a String.
The create a template expression, we need two things:
- A template processor
- A template containing wrapped expressions like
\{name}
These two requirements are combined by a dot, almost like a method call. Using one of the previous examples, it looks like this:
The first question you might have is: where does STR
come from?
As String -> String
templates are most likely the default use case for String templates, the template processor STR
is automagically imported into every Java source file.
So all the inconvenience added by Java’s approach is 4 additional characters.
Multi-Line Templates and Expressions
Template expressions also work with text blocks (Java 15+):
Not only the template itself can be multi-line, expressions can be too, including comments!
Be aware though, that the expression still needs to be like a single-line lambda, not a code block.
More Than Just String
The main advantage of Java’s implementation over other languages in my opinion is the possibility of using another template processor than a String -> String
one.
Look at the JSON example, again.
Wouldn’t it be nicer if the interpolation could return a JSONObject
and not a String
?
So let’s do that!
Creating Your Own Template Processor
Template processing is built upon the newly added nested functional interface java.lang.StringTemplate.Processor
:
How the processing works is that the String literal containing the expression is converted to a StringTemplate
and given to a Processor
.
If we want to create a JSONObject
, we need to interpolate the String literal first, and then create the new instance of the desired return type.
Using the static
helper Processor.of
makes this quite easy:
But that’s not the real power of a custom Processor
.
The StringTemplate
gives us more than just an argument-less interpolate
method.
We have access to the expression results and can manipulate them!
That means the template can be simplified, as the Processor
will be responsible for handling the values correctly, like escaping double quotes in the user value, etc.
This is the desired template we try to use:
To achieve this, the Processor
evaluates the results of the expressions (template.values()
) and creates new replacements to be matched with fragment literals (template.fragments()
):
That’s it!
All the logic required to build a JSONObject
from a String template in a single place, and we can safely use any expression in a JSON template and don’t need to think about quoting or not.
Endless Possibilities
As we have access to the fragments and values, we can create whatever we want.
The previous “Bobby Tables” fiasco can be avoided by composing SQL queries with a sanitized Processor
.
Or a Processor
that has access to the current Locale
could be used for i18n purposes.
Whenever we have a String-based template that requires transformation, validation, or sanitizing, Java’s String templates will give us a built-in simplistic template engine without requiring a third-party dependency.
To not start from zero, the Java platform provides two additional template processors besides STR
.
Be aware that the additional template processors seem to be missing-in-action in Java 21.ea.27. At least I didn’t get it to work in my test setup.
The processor FMT
combines the interpolation power of STR
with the format specifiers defined in java.util.Formatter
:
The third Processor
provided by the Java platform is RAW
, which doesn’t interpolate and returns a StringTemplate
instead.
Should I use a Preview Feature?
In my opinion, it highly depends on where you want to use it. Preview features are always subject to change, so you might need to fix your code after each new JDK release. Be prepared that there might be bugs or that it’s not complete yet!
However, in internal code, I don’t see a big issue in trying out new features. But remember that anyone using such code requires to enable the preview features, too, so don’t “force” this decision onto them.
Conclusion
Java’s String Templates are another prime example of Java “doing its thing” and giving us a missing feature compared to other languages but with a twist!
Instead of simply copying another language to provide the most convenient variant for the most obvious use cases, Java sacrifices a little bit of convenience by requiring four additional characters for the “default” case. In return, however, we received a flexible and easy-to-use simplistic template engine that’s built right into the JDK.
I, for one, can’t wait for the feature to leave preview status.
Resources
Looking at Java 21
- Intro
- String Templates (JEP 430)
- Simpler Main Methods and Unnamed Classes (JEP 445)
- Sequenced Collections (JEP 431)
- Scoped Values (JEP 446)
- Switch Pattern Matching (JEP 441)
- Feature Deprecations (JEP 449, 451)
- Record Pattern Matching (JEP 440)
- Generational ZGC (JEP 439)
- Virtual Threads (JEP 444)
- The Little Things