String Templates (JEP 430)
String templates, previewed in Java 21, are a mechanism for producing objects from templates that contain string fragments and embedded expressions. The syntax is different from that of other languages, but the differences are minor and make sense for Java. The JDK provides processors for plain interpolation and formatted output. It is easy to implement your own processors.
String Interpolation
Many strings are composed of fixed and variable parts. In Java, such strings are traditionally formed through concatenation:
String message = "Hello, " + name + "! Next year, you'll be " + (age + 1) + ".";
That is tedious to read and write, and a bit error-prone since it is easy to mess up spaces and quotation marks.
Formatted output can be a better alternative:
message = "Hello, %s! Next year, you'll be %d.".formatted(name, age + 1);
MessageFormat
is similar, but optimized for internationalization:
message = MessageFormat.format("Hello, {0}! Next year, you''ll be {1,choice,0≤{1}|50<even wiser}.", name, age + 1);
In both cases, the contents of the composite string is easier to make out, but the variable parts are separated from their final location.
Many programming languages provide interpolation of expressions that are embedded inside a string. (The Merriam-Webster dictionary defines “interpolate” as “to insert between other things or parts”.) Java 21 adds a preview feature with the same capability. Here is how it looks like:
message = STR."Hello, \{name}! Next year, you'll be \{age + 1}.";
STR
is a template processor and "Hello, \{name}! Next year, you'll be \{age + 1}."
is a string template with embedded expressions \{name}
and \{age + 1}
. The entire expression on the right hand side of the assignment is a template expression.
Here is a sandbox to play with these alternatives.
Template Expressions
A template expression starts with an expression whose value is a template processor, an instance of a class that implements the StringTemplate.Processor
interface. That interface has a single method process
.
The template processor is followed by a dot, which the JEP describes unhelpfully as “a dot character (U+002E), as seen in other kinds of expressions.” Why a dot? Perhaps to remind the reader that this is a shorthand for a method call:
STR.process(templateArgument)
To the right of the dot is the template argument, which can be one of four things:
- A string template (as in the example)
- A text block template
- A string literal
- A text block
A string template is delimited by single quotes, just like a string literal, but it contains at least one embedded expression. (As it happens, \{
is not a valid escape sequence in a string literal.)
Similarly, a text block template is just like a text block, but with embedded expressions. Here is an example:
STR.""" <div> <p class="greeting">Hello, \{name}!</p> <p>Next year, you'll be \{age + 1}.</p> </div>"""
In both string templates and text block templates, you can add comments and line breaks inside embedded expressions:
String message = STR."Hello, \{ name /* Ok to add a comment */ }! Next year, you'll be \{ age + 1 // Ok to add line breaks }.";
A template is not a String
. It is an instance of the StringTemplate
class. You will see soon what that class does.
Unlike a string literal or text block, a template is only a valid expression when it follows a template processor and a dot. The processor is responsible for turning the text and embedded expressions of the template into a result. Without a processor, a template doesn’t have a value.
You are also allowed to place a string literal or text block after a template processor and a dot. You can regard that as the special case of a template with zero embedded expressions. It can also be a temporary starting point that is later turned into a template:
message = STR."Hello, TODO! Next year, you'll be TODO.";
This sandbox shows the four kinds of template arguments.
Predefined Template Processors
The Java 21 API contains three template processors: the interpolating STR
processor that you have already seen, as well as processors for formatting and for obtaining an unprocessed template.
The STR
processor, a static field of the class java.lang.StringTemplate
is special—it is always imported. In other words, every Java file has these automatic imports:
import java.lang.*; import static java.lang.StringTemplate.STR;
The other processors must be imported manually:
import static java.util.FormatProcessor.FMT; import static java.lang.StringTemplate.RAW;
The FMT
processor carries out printf
style formatting:
String line = FMT."%-20s\{item.description} | %5d\{item.quantity} | %10.2f\{item.price}%n";
The format specifiers must immediately precede the embedded expressions.
The RAW
processor yields the template argument as a StringTemplate
instance:
StringTemplate template = RAW."Hello, \{name}! Next year, you'll be \{age + 1}.";
See the following section for more information about the StringTemplate
class.
Note that a template processor can produce an object of any class. The FMT
processor yields a String
, but the RAW
processor yields an object of the class StringTemplate
.
Here is a sandbox with these examples. Try adding a space before or after a format specifier. Also try reassigning item
after the raw template was formed. Are the values updated? Should they be?
The StringTemplate
class
When a template expression is evaluated, the template arguments are placed in an object that is then passed to the process
method of the template processor.
The name StringTemplate
is perhaps unfortunate. Any of the four possible template argument types is turned into a StringTemplate
instance. TemplateArguments
might have been a better choice for the class name.
A StringTemplate
instance stores this information:
- The fragments; that is, the strings between the embedded expressions
- The values of the embedded expressions
The fragments
and values
methods yield lists of fragments and values.
String name = "Fred"; int age = 42; StringTemplate template = RAW."Hello, \{name}! Next year, you'll be \{age + 1}."; template.fragments() // ["Hello, ", "! Next year, you'll be ", "."] template.values() // ["Fred", 43]
Note that there is one more fragment than there are values.
The static interpolate
method merges a list of fragments and values. As you will see in the next section, this is useful for implementing your own template processors.
I don’t think that many programmers will create StringTemplate
instances, but there are three ways to do so:
- With the
RAW
processor:RAW."Hello, \{name}!"
- With the static
of
methods:StringTemplate.of(List("Hello, ", "!"), List(name)) StringTemplate.of("Hello, Sailor!")
- With the static
combine
methods:StringTemplate.combine(RAW."Hello, \{name}! ", RAW."Next year, you'll be \{age + 1}.")
There are two combine
methods, with parameter types StringTemplate...
and List<StringTemplate>
.
Finally, the process
method lets you apply a processor, just in case you prefer
template.process(processor)
over
processor.process(template)
This sandbox shows the API in action, even though you probably only need to worry about the first two methods.
Writing Your Own Template Processor
The process
method of a template processor turns StringTemplate
instances into objects. Since StringTemplate.Processor
is a functional interface, you can construct an instance from a lambda expression. Here is a simple example that places the values in boxes:
StringTemplate.Processor
is a generic class with two type parameters: the result type and the type of the exception that the processor can throw.
Here, the values are transformed and then interpolated with the fragments. The interpolate
method is helpful in this common situation:
public static StringTemplate.Processor<String, RuntimeException> BOX = template -> StringTemplate.interpolate(template.fragments(), template.values().stream().map(v -> "[" + v + "]").toList());
As an aside, in the JEP, all template processors are written in uppercase, even if they are not static variables. This is not a requirement. The processor is an expression like any other. If a processor is a local variable, or a method invocation, it is perfectly fine to use lowercase letters. For example:
public static StringTemplate.Processor<String, RuntimeException> box(String left, String right) { return template -> StringTemplate.interpolate(template.fragments(), template.values().stream().map(v -> left + v + right).toList()); } ... String result = box("{", "}")."Hello, \{name}! Next year, you'll be \{age + 1}.";
String processors can protect against injection attacks. When creating HTML, XML, JSON, SQL, and so on, from arbitrary values, you want to escape special characters such as quotation marks and bracket delimiters. The JEP has a useful example of using a PreparedStatement
for a SQL query. This sandbox shows a simpler example for processing XML.
Now it’s your turn. Complete the code below so that one can write a Map<String, Object>
as
MAP."key1 \{value1} key2 \{value2} ..."
Trim the fragments and ignore the last one.
Show Solution
public static StringTemplate.Processor<Map<String, Object>, RuntimeException> MAP = template -> { List<Object> values = template.values(); var result = new HashMap<String, Object>(); for (int i = 0; i < values.size(); i++) result.put(template.fragments().get(i).trim(), values.get(i)); return result; };
Conclusion
String templates provide interpolation in a way that should be familiar to anyone who has used this feature in another programming language. Here are the highlights:
- Embedded expressions are enclosed in
\{...}
. This makes sense for Java since\{
has previously not been a valid escape sequence in strings. - Template expressions have the form
processor.argument
, where the argument is a string template, text block template, string, or text block. - To write your own template processor, provide a method that turns the fragments and values of a
StringTemplate
instance into an object.