Looking at Java 21: String Templates

 · 11 min
Dim Hou on Unsplash

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.


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 and StringBuilder
  • String::format and String::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:

java
var name = "Ben";
var tempC = 28;

var greeting = "Hello " + name + ", how are you?\nIt's " + tempC + "°C today!";

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:

java
var greeting = new StringBuilder().append("Hello ")
                                  .append(name)
                                  .append(", how are you?\nIt's")
                                  .append(tempC)
                                  .append("°C today!")
                                  .toString();

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:

java
var format = "Hello %s, how are you?\nIt's %d°C today!";
var greeting = String.format(format, name, tempC);

// Java 15+
var greeting = format.formatter(name, tempC);

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:

java
var format = new MessageFormat("Hello {0}, how are you?\nIt's {1}°C today!");
var greeting = format.format(name, tempC);

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:

groovy
def greeting = "Hello ${this.user.firstname()}, how are you?\nIt's ${tempC}°C today!";

Especially paired with multi-line Strings, interpolation trumps any concatenation approach in readability and simplicity:

groovy
def json = """
{
    "user": "${this.user.firstname()}",
    "temperatureCelsius: ${tempC}
}
"""

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:

  1. Evaluate expression/variable
  2. Convert to String value if needed
  3. 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.

XKCD 327: Exploits of a Mom
XKCD 327: Exploits of a Mom (Source)

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:

java
var name = "Ben";
var tempC = 28;

var greeting = STR."Hello \{this.user.firstname()}, how are you?\nIt's \{tempC}°C today!";

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

java
var json = STR."""
{
    "user": "\{this.user.firstname()}",
    "temperatureCelsius: \{tempC}
}
""";

Not only the template itself can be multi-line, expressions can be too, including comments!

java
var json = STR."""
{
  "user": "\{
    // We only want to use the firstname
    this.user.firstname()
  }",
  "temperatureCelsius: \{tempC}
}
""";

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:

java
@FunctionalInterface
public interface Processor<R, E extends Throwable> {

  R process(StringTemplate stringTemplate) throws E;

  static <T> Processor<T, RuntimeException> of(Function<? super StringTemplate, ? extends T> process) {
    return process::apply;
  }

  // ...
}

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:

java
/// CREATE NEW TEMPLATE PROCESSOR
var JSON = StringTemplate.Processor.of(
  (StringTemplate template) -> new JSONObject(template.interpolate())
);

// USE IT LIKE BEFORE
JSONObject json = JSON."""
{
  "user": "\{
    // We only want to use the firstname
    this.user.firstname()
  }",
  "temperatureCelsius: \{tempC}
}
""";

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:

java
JSONObject json = JSON."""
{
  "user": \{this.user.firstname()},
  "temperatureCelsius: \{tempC}
}
""";

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

java
StringTemplate.Processor<JSONObject, JSONException> JSON = template -> {
  String quote = "\"";
  List<Object> newValues = new ArrayList<>();

  for (Object value : template.values()) {
    if (value instanceof String str) {
      // SANITIZE STRINGS
      // the many backslashes look weird, but it's the correct regex
      str = str.replaceAll(quote, "\\\\\"");
      newValues.add(quote + str + quote);
    }
    else if (value instanceof Number || value instanceof Boolean) {
      newValues.add(value);
    }
    // TODO: support more types
    else {
      throw new JSONException("Invalid value type");
    }
  }

  var json = StringTemplate.interpolate(template.fragments(), newValues);

  return new JSONObject(json);
};

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:

java
record Shape(String name, int corners) { }

var shapes = new Shape[] {
  new Shape("Circle", 0),
  new Shape("Triangle", 3),
  new Shape("Dodecagon", 12)
};

var table = FMT."""
  Name     Corners
  %-12s\{shapes[0].name()}  %3d\{shapes[0].corners()}
  %-12s\{shapes[1].name()}  %3d\{shapes[1].corners()}
  %-12s\{shapes[2].name()}  %3d\{shapes[2].corners()}
  \{" ".repeat(7)} Total corners %d\{
    shapes[0].corners() + shapes[1].corners() + shapes[2].corners()
  }
""";

// OUTPUT:
// Name        Corners
// Circle         0
// Triangle       3
// Dodecagon     12
//        Total: 15

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.


A Functional Approach to Java Cover Image
Interested in using functional concepts and techniques in your Java code?
Check out my book!
Available in English, Polish, Korean, and soon, Chinese.

Resources

Looking at Java 21