IntelliJ IDEA
IntelliJ IDEA – the Leading Java and Kotlin IDE, by JetBrains
String Templates in Java – why should you care?
TLDR; The existing String concatenation options are difficult to work with and could be error prone. String Templates (a preview feature introduced in Java 21) greatly improves how we create strings in Java by merging constant strings with variable values.
The name of this feature pretty much says it all – String Templates. Think of it as patterns that include String literals and variable values. The variable values could be either variables, expressions or method calls, and their values are injected at runtime.
If you are wondering what the code looks like, here’s an example that uses String Templates to create multiline strings with literals and variable values, passing it to logger.info()
:
logger.info(STR.""" Order processed successfully: Order ID: \{orderId}, placed on \{orderDate} \{product}, \{quantity} (\{product.contains("pens") ? "dozens" : "units"}) Total Price: \{calculatePrice(product, quantity)}""");
Don’t worry if it doesn’t make much sense now. We’ll cover the details in this blog post.
What are the benefits?
For starters, this feature simplifies string creation – you no longer need to use concatenation operators (like ‘+’), especially when working with longer or multi-line strings. In the example above, the multiline string includes a combination of String literals and variable values (via variables, method calls or even code snippets that return a value like the ternary operator). By including the variable values within the String, it is clearer where its value will be inserted. Sure, there are alternatives like StringBuilder
, StringBuffer
, and MessageFormat
, but they come with their own issues (as we’ll see in this blog post).
String Templates offer more than just the convenience of reading and writing code with String concatenation. They provide a layer of security when you are mixing variable values with String constants. You can create custom String templates to decipher letter combinations, either ignoring them or replacing them for added security.
What’s unfortunate, though, is that most discussions and resources I’ve come across tend to fixate solely on the initial benefit, often overlooking or not giving due credit to the other powerful features. In this blog post, I’ll highlight the benefits of all these features, via hands-on coding examples.
String concatenation – a common, but uninteresting and error prone task
Let’s talk about a common use case. Imagine while working with an online shopping application, you need to log a message with order details, if the quantity for a product in an order is 0 or negative. It is common to create a String that includes literals, such as, ‘Invalid order’, and order details that can be accessed using variables like orderId, etc. Here’s a sample code to accomplish this (focus on the concatenated String):
public void processOrder(int orderId, String product, int qty, LocalDate orderDate) { if (quantity <= 0) { String errorMessage = "Invalid order quantity: " + qty + " for product " + product + ", order ID " + orderId; logger.error(errorMessage); return; } //.. Remaining code }
The code seems harmless. However, I’ve often missed adding spaces before and after the literal text values in similar code, generating a log message similar to the following that is hard to read:
Invalid order quantity: -5for productWidget,order ID12345
It isn’t an issue to fix such code. The red flag is to see similar repetitions by new and experienced developers A LOT of times. Have you done this or experienced it too?
Another major issue with using the concatenation operator is that such code becomes difficult to read or understand quickly when you add more String literals or variable values. Also, I don’t think any developer wants to spend any time of their life counting the opening and closing String quotes or inserting the + operator (do you?). So, what are the existing alternatives and how good are they?
Alternatives for the + operator
I’ll cover three alternatives in this section: StringBuilder’s append()
method, String’s formatted()
method and MessageFormat’s format()
method.
You could use StringBuilder append()
calls as shown below. The code is easier to read, but it is much longer now. Worse, it still doesn’t help much with spotting the missing spaces (invoke context actions anywhere within the string value in IntelliJ IDEA to access the popup menu):
Let’s try the next option, that is, replace the original String concatenation using the +
operator with the formatted()
method in the String class. Don’t worry about the syntax, invoke the context actions and select it from the popup menu, as follows:
It solved one issue and introduced another. Now you can clearly see where the literal strings have spaces or a comma. But now you need to refer back and forth between the placeholders %d
, %s
and the variable names, to find out which variable value will be inserted where.
Let’s check if using our last alternative, the format()
method in the MessageFormat
class solves this newly introduced issue and the older issue of code clarity, as follows:
Tabular comparison of existing String concatenation options
For your convenience, here’s a table listing all the options we covered in previous sections so that you could compare them and see for yourself:
+ operator | "Invalid order quantity: " + qty + " for product " + product + ", order ID " + orderId |
StringBuilder append() |
new StringBuilder().append("Invalid order quantity: ") .append(qty) .append(" for product ") .append(product) .append(", order ID ") .append(orderId) .toString(); |
String formatted() | "Invalid order quantity: %d for product %s, order ID %d".formatted(qty, product, orderId) |
MessageFormat format() | MessageFormat.format("Invalid order quantity: {0} for product {1}, order ID {2}", qty, product, orderId) |
Let’s see if String Templates has a better solution to offer!
Hello String Templates!
IntelliJ IDEA 2023.2.2 and later versions can detect code that uses the + operator to combine String literals and variable values, and nudges that you could use String Templates instead (with just a click of an option).
This is my favorite part of using IntelliJ IDEA. It allows you to use a newer Java feature without requiring you to know even the syntax or any of its details. How cool is that!
I’ll cover the different parts of a String Template soon. Until then, check out how IntelliJ IDEA can help you use this amazing feature in the following gif:
If this option to use String Templates doesn’t show up in your version of IntelliJ IDEA, configure it using the instructions below.
IntelliJ IDEA Configuration
Basic support for Java 21 is available since IntelliJ IDEA version 2022.2.2. More support is added in its “2023.3 release, due for release very soon.
To use String Templates, go to ProjectSettings | Project, set the Project SDK to 21 and set Project language level to ‘‘21 (Preview) – String templates, unnamed classes and instance main methods etc.’:
You can use JDK 21 if you have already downloaded it on your system, or download it by clicking on ‘Edit’ and then selecting ‘Add SDK >’, followed by ‘Download JDK 21…’. You can choose from a list of vendors.
On the Modules tab, ensure the same language level is selected for the modules – ‘21 (Preview) – String templates, unnamed classes and instance main methods etc.:
Once you select the language level mentioned above, you might see a pop-up which informs you that IntelliJ IDEA might discontinue the support for the Java preview language features in its next versions. Since a preview feature is not permanent (yet), and it is possible that it could change (or even be dropped) in a future Java release.
Anatomy of String Templates
Before we cover more details, the image below shows how the different parts of a String Template are referred to for the following code:
STR."Invalid order quantity : \{qty}";
Template Processor: STR
. It is a predefined template processor defined in the Java API.
Template: Everything within " "
or """ """
is a template
Embedded expressions: Each occurrence of dynamic data, such as, \{qty}
is embedded expressions.
This example mentions STR
as a String Template processor. The core Java API has other templates processors too, such as FMT
covered later in this blog post.
Using String Templates with Text Blocks = Wow!
The preceding examples show String concatenation with a single line of code. However, often you need to work with multiline strings with embedded variable values. Text blocks work great with multiline literal String values, but not when you need to embed variable values in it.
Imagine you need to log a more details message like the following, for a successfully completed order:
logger.info("Order processed successfully: " + "Order ID: " + orderId + ", placed on " + orderDate + " " + product + ", " + quantity + " (" + (product.contains("pens") ? "dozens" : "units") + ")" + "Total Price: " + calculatePrice(product, quantity));
Imagine it also needed additional formatting information, such as the newline:
logger.info("\nOrder processed successfully: " + "\nOrder ID: " + orderId + ", placed on " + orderDate + "\n" + product + ", " + qty + " (" + (product.contains("pens") ? "dozens" : "units") + ")" + "\nTotal Price: " + calculatePrice(product, qty));
The following code is quite difficult to read when text blocks are used with the concatenation operator (since the opening """
in text blocks must be followed by a newline):
logger.info(""" Order processed successfully: Order ID: """ + orderId + """ , placed on """ + orderDate + product + """ , """ + qty + """ (""" + (product.contains("pens") ? "dozens" : "units") + """ ) Total Price: """ + calculatePrice(product, qty));
Of course, you could use either the formatted()
method in the String
class or the format()
method from class MessageFormat
, but using String templates with text blocks is a much better idea – they are much more readable:
logger.info(STR.""" Order processed successfully: Order ID: \{orderId}, placed on \{orderDate} \{product}, \{qty} (\{product.contains("pens") ? "dozens" : "units"}) Total Price: \{calculatePrice(product, qty)}""");
Apart from using variables and method calls, did you notice in the preceding code block that you can also embed code snippets that return a value like the ternary operator as an embedded expression?
Embedded expressions in String Templates and IntelliJ IDEA
Inserting embedded expressions in longer String templates is usually challenging without help. Don’t worry, IntelliJ IDEA has your back!
Each embedded expression must be enclosed within \{}
. When you type \{
, IntelliJ IDEA adds the closing ‘}
’ for you. It also offers code completion to help you select a variable in scope, or any methods on it. If the code that you insert doesn’t compile, IntelliJ IDEA will highlight that too (as a compilation error), as shown in the following gif:
Security and custom String Templates
Imagine a vulnerability situation where your applications log files could be forged. Depending on how the messages are logged and interpreted, this could result in a severe attack.
For an example, imagine your application logs a message when it detects multiple unsuccessful attempts from the same username. To keep the code simple, the following code logs a message and a username to a log file:
private void recordFailedAttempt(String username) { logger.warn(STR."Multiple unsuccessful login attempts : \{username}"); //.. rest of code }
Under regular circumstances, you might see a log message like the following, if an unsuccessful login attempt is made by the username – tester404:
2023-11-23 17:15:15 WARN LogForgingDemo:54 - Multiple unsuccessful login attempts : tester404
Imagine a hacker tries to (unsuccessfully) login to your application using the username “tester404\n[SECURITY\tBREACH\t-\tALL\tDATA\tLEAKED]\tSHUTDOWN\tSYSTEMS\tNOW
“. This is how your log file might look:
– log.txt – 2023-11-23 17:20:48 WARN LogForgingDemo:54 - Multiple unsuccessful login attempts : tester404 [SECURITY BREACH - ALL DATA LEAKED] SHUTDOWN SYSTEMS NOW
Due to the newline in the username passed to the method recordFailedAttempt()
, the log file seems to have two messages – one related to the unsuccessful login attempt and another about a potential data leak. If the person who monitor’s your applications log file is unaware about such log file forging issues, they might actually act on the log message and shut down your system!
There are multiple ways to address it, such as, using regex in your log file configurations. In this example, let’s try to address it by creating a custom String Template that can decipher special character combinations, such as, the new line, and act on it accordingly. The first step is to create an implementation of the interface StringTemplate.Processor
:
StringTemplate.Processor<String, RuntimeException> LOG_STR = st -> { StringBuilder sb = new StringBuilder(); Iterator fragIter = st.fragments().iterator(); for (Object value : st.values()) { sb.append(fragIter.next()); // Ignore \n and the text that follows it. Most probably it is Log Forging. String temp = value.toString(); if (temp.contains("\n")) temp = temp.substring(0, temp.indexOf("\n")); sb.append(temp); } sb.append(fragIter.next()); return sb.toString(); };
Now, you could use the preceding custom String template instead of STR
in your application, as follows:
private void recordFailedAttempt(String username) { logger.warn(LOG_STR."Multiple unsuccessful login attempts : \{username}"); //.. rest of code }
When you use the preceding code, the same username, that is, “tester404\n[SECURITY\tBREACH\t-\tALL\tDATA\tLEAKED]\tSHUTDOWN\tSYSTEMS\tNOW
“, would ignore the text following \n and log a message similar to the following:
2023-11-23 16:29:34 WARN LogForgingDemo:54 - Multiple unsuccessful login attempts : tester404
If you like this example, you might be wondering how to create your own custom String template. It is quite easy given that the core Java API classes already do the heavy lifting for you.
When you create a String by merging string literals and variables, the String literals and variable values are stored as List of String and Objects (defined in the interface StringTemplate). To create the final String, you need to add one each from these Lists in an alternate manner. Before adding the value, handle the issue. For example, to ignore the new line character and everything else that followed it, I determined if a value included the new line character and then discarded everything that followed it.
Again, this example is deliberately made simple to highlight the process of creating custom templates. In the real world cases, you might want to deal with or take care of multiple use cases.
Other classic examples, where custom String templates could be really helpful are – to prevent SQL Injection Attacks, XSS Attacks, Command injection attacks, and many more. All these use cases use the values entered by the user to execute commands on the host system. String Templates could help you identify and remove the malicious values, right before you concatenate them to create a database command, OS command and others.
More examples of interesting custom String Templates
Last month, my daughter asked me if I could help her with her Chemistry exam preparation. Of course, I agreed and started off as a great problem solver (true developers always create applications to solve all problems). I created a Java application to generate a set of objective questions, which were added to a pdf file that could be sent to a printer.
I soon ran into a problem – the PDF library that I was using didn’t recognize a few symbols, such as the down arrow used with chemical equations to mark precipitation.
To get around it, I created a custom String Template, which would decipher the down arrow in the question text, which was read in the memory as a record instance from a json file. However, while generating the corresponding PDF file, the String template would replace the precipitation symbol, that is, ↓
, with the text ‘(Precipitation)
’. Here’s the code for this custom String template:
var PDF_STR = StringTemplate.Processor.of((StringTemplate st) -> { StringBuilder sb = new StringBuilder(); Iterator fragIter = st.fragments().iterator(); for (Object value : st.values()) { sb.append(fragIter.next()); String temp = value.toString().replace("↓", "(Precipitation)"); sb.append(temp); } sb.append(fragIter.next()); return sb.toString(); });
As I mentioned in the previous section, when you create a String by merging string literals and variables, the String literals and variable values are stored as List of String and Object (defined in the interface StringTemplate
). To create the final String, you need to add one each from these Lists in an alternate manner. Before adding the value, handle the issue. For example, in the preceding code snippet, I replace every occurrence of “↓” with “(Precipitation)”.
To use this custom String template, I should use PDF_STR
instead of STR
to merge String literals and variables. Here’s an example:
pdfContent.newLine(PDF_STR."(\{++questionNumber}) \{question.question()}");
This doesn’t end here. I was quite happy with my Custom String Template and explained it to my daughter and she asked me if I could create another one that could decipher an emoji sign language (when entered as variable text) and convert them into real emojis. For example, replace :) with 🙂, and others. So here it is (again keeping the code simple):
static Map<String, String> emojiMap = new HashMap<>(); static { emojiMap.put(":)", "\uD83D\uDE42"); // Smiling face emojiMap.put(";)", "\uD83D\uDE09"); // Winking face emojiMap.put(":(", "\uD83D\uDE41"); // Frowning face emojiMap.put("o)", "\uD83D\uDE07"); // Angel face emojiMap.put(":D", "\uD83D\uDE00"); // Grinning face // add others (https://unicode.org/emoji/charts/full-emoji-list.html) } static StringTemplate.Processor<String, RuntimeException> EMOJIS = st -> { StringBuilder sb = new StringBuilder(); Iterator fragIter = st.fragments().iterator(); for (Object value : st.values()) { sb.append(fragIter.next()); String temp = value.toString(); // assuming that the key value contains just the emoji as text if (emojiMap.containsKey(temp)) temp = emojiMap.get(temp); sb.append(temp); } sb.append(fragIter.next()); return sb.toString(); };
Assume you execute the following code:
String smile = ":)"; String wink = ";)"; System.out.println(EMOJIS."Smiling: \{smile}, Wink: \{wink}");
Here’s the output of this code:
Smiling: 🙂, Wink: 😉
So which interesting String Templates do you plan to create today?
FMT String Template
We’ve worked with examples of using the STR String templates from the Java API. The core Java API also defines another String Template: FMT. You could think of it as using the ability to use the format specifiers with options, such as, %d, %s and others for finer control over how your values are displayed. For example, you could define the count of digits before and after a decimal place for decimal values, or the count of letters that should be used for string values.
Such control could be quite handy to create formatted outputs. Imagine you need to print a receipt similar to the following on a plain paper at an art supplies store:
-------------------------------------------------------------------------------------- Your Neighbourhood Art Supplies Store -------------------------------------------------------------------------------------- Date: 2023-10-20 Invoice Number: 12345 Customer Details Name: John Smith Address: 123 Main Street City: Smallville Phone: 555-123-4567 -------------------------------------------------------------------------------------- S.No. Item Name Quantity Unit Price($) Total($) -------------------------------------------------------------------------------------- 1 Acrylic Paint Set 1 20.00 20.00 2 Watercolor Brushes 5 15.00 75.00 3 Sketchbook 12 10.00 120.00 4 Oil Paints Set 1 25.00 25.00 5 Canvas Panels (5-pack) 6 12.00 72.00 -------------------------------------------------------------------------------------- Subtotal: $82.0 Sales Tax (6%): $4.92 Total Amount: $86.92; -------------------------------------------------------------------------------------- Thank you for shopping with us! --------------------------------------------------------------------------------------
For the preceding receipt, the variable values like the customer details and the product details must use a fixed count of places so they don’t overflow. Assuming that Order and ShippingItem are defined as follows:
public record Order ( String date, int invoiceNumber, String customerName, String customerAddress, String customerCity, String customerPhone, ShoppingItem[] items) {} record ShoppingItem ( String name, int quantity, double price) {}
And initialized as follows:
ShoppingItem[] items = { new ShoppingItem("Acrylic Paint Set", 1, 20.00), new ShoppingItem("Watercolor Brushes", 5, 15.00), new ShoppingItem("Sketchbook", 12, 10.00), new ShoppingItem("Oil Paints Set", 1, 25.00), new ShoppingItem("Canvas Panels (5-pack)", 6, 12.00) }; Order order = new Order("2023-10-20", 12345, "John Smith", "123 Main Street", "Smallville", "555-123-4567", items);
The following code could be used to output the bill shown above:
public static void outputBillForOrder(Order order) { String billOutput = FMT.""" --------------------------------------------------------------------------------- Your Neighbourhood Art Supplies Store --------------------------------------------------------------------------------- Date: %-22s\{order.date()} Invoice Number: %-22s\{order.invoiceNumber()} Customer Details Name: %-22s\{order.customerName()} Address: %-22s\{order.customerAddress()} City: %-22s\{order.customerCity()} Phone: %-22s\{order.customerPhone()} -------------------------------------------------------------------------------------- S.No. Item Name Quantity Unit Price($) Total($) -------------------------------------------------------------------------------------- """; double subtotal = 0.0; for (int i = 0; i < order.items().length; i++) { ShoppingItem item = order.items()[i]; billOutput += FMT."%-8s\{i + 1} %-33s\{item.name()} %8d\{item.quantity()} %17.2f\{item.price()} %17.2f\{item.quantity() * item.price()}\n"; subtotal += item.price(); } double salesTaxRate = 0.06; double salesTax = subtotal * salesTaxRate; double totalAmount = subtotal + salesTax; billOutput += STR.""" -------------------------------------------------------------------------------------- Subtotal: $\{subtotal} Sales Tax (6%): $\{salesTax} Total Amount: $\{totalAmount}; -------------------------------------------------------------------------------------- Thank you for shopping with us! --------------------------------------------------------------------------------------"""; System.out.println(billOutput); }
Note how the preceding example uses both the Template Processors FMT and STR.
Preview Features
String Templates were introduced as a preview language feature in Java 21. They’ll continue to be in its second preview for Java 22. With Java’s new release cadence of six months, new language features are released as preview features. They may be reintroduced in later Java versions in the second or more preview, with or without changes. Once they are stable enough, they may be added to Java as a standard language feature.
Preview language features are complete but not permanent, which essentially means that these features are ready to be used by developers, although their finer details could change in future Java releases depending on developer feedback. Unlike an API, language features can’t be deprecated in the future. So, if you have feedback about any of the preview language features, feel free to share it on the JDK mailing list (free registration required).
Because of how these features work, IntelliJ IDEA is committed to only supporting preview features for the current JDK. Preview language features can change across Java versions, until they are dropped or added as a standard language feature. Code that uses a preview language feature from an older release of the Java SE Platform might not compile or run on a newer release.
Summary
String Templates is a great addition to Java. Apart from helping developers to work with strings that combine string constants and variables, they provide a layer of security. Custom String templates can be created with ease to accomplish multiple tasks, such as, to decipher letter combinations, either ignoring them or replacing them for added security.
I wish more custom templates were added to the Core Java API so that more developers could use it to make their code easy to read and write with added security.