Java String Templates and Text Blocks: Interpolation and Multi-line Strings
Java 15 introduced text blocks for multi-line strings (finalized), making SQL, JSON, and HTML literals readable. Java 21 introduced String Templates as a preview feature (JEP 430), providing safe interpolation with template processors like STR and FMT. Note: String Templates were subsequently withdrawn after preview and redesigned. Always check your JDK version. For stable interpolation today, formatted() or String.format() remain the standard approach.
Contents
Text Blocks: Java 15+
Text blocks use triple-quote delimiters (""""""\s\
// Text blocks — triple-quoted strings, finalized in Java 15
// Leading whitespace is stripped based on the least-indented line
String json = """
{
"name": "Alice",
"age": 30,
"active": true
}
""";
System.out.println(json);
// {
// "name": "Alice",
// "age": 30,
// "active": true
// }
// SQL with text block — much more readable
String query = """
SELECT u.name, o.total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.active = true
AND o.total > 100
ORDER BY o.total DESC
""";
// HTML template
String html = """
<div class="card">
<h2>%s</h2>
<p>%s</p>
</div>
""".formatted("Title", "Body text");
// Incidental whitespace stripping — position of the closing """ matters
// Closing """ on same indentation level removes that much leading whitespace
// Explicit newline control:
String noTrailingNewline = """
hello\
world"""; // backslash at end of line = no newline; "helloworld"
String spacer = """
a \s
b"""; // \s preserves trailing space before newline
// String escape sequences still work inside text blocks
String escaped = """
Tab:\there, Quote: \"quoted\"
""";
String Templates Preview: Java 21
The STR\{expression}RAWStringTemplateFMTSTRFMT
// String Templates — Preview in Java 21 (JEP 430)
// NOTE: Withdrawn after preview, redesign ongoing — not in Java 23+
// Only available with --enable-preview in Java 21/22
// STR template processor — simple string interpolation
// Syntax: STR."template \{expression} text"
String name = "Alice";
int age = 30;
String greeting = STR."Hello, \{name}! You are \{age} years old.";
System.out.println(greeting); // Hello, Alice! You are 30 years old.
// Expressions (not just variables)
double price = 9.99;
String receipt = STR."Total: $\{price * 1.1:.2f}"; // expression with format
// Multi-line template with STR
String json = STR."""
{
"name": "\{name}",
"age": \{age},
"adult": \{age >= 18}
}
""";
// FMT template processor — applies printf-style format specifiers
import static java.util.FormatProcessor.FMT;
String formatted = FMT."Price: $%,.2f\{price} | Tax: $%,.2f\{price * 0.1}";
// Key safety advantage over String.format — embedding in SQL is validated by processor
// (Custom processors can reject SQL injection, validate HTML, etc.)
// Unlike: "SELECT * FROM users WHERE name = '" + name + "'" // SQL injection risk!
// Available processors in Java 21 preview:
// STR — basic interpolation, always produces String
// FMT — interpolation + printf format specifiers
// RAW — produces StringTemplate object (for custom processors)
--enable-previewformatted()String.format()Custom Template Processors
A template processor implements StringTemplate.ProcessorStringTemplatefragments()values()
// Custom template processor — implements StringTemplate.Processor
// Useful for: SQL safe interpolation, HTML escaping, JSON building
import java.lang.StringTemplate;
// SQL injection-safe processor
StringTemplate.Processor<PreparedStatement, SQLException> SQL = st -> {
// st.fragments() — the literal string parts between interpolations
// st.values() — the interpolated values
String sql = String.join("?", st.fragments()); // replace values with ?
PreparedStatement ps = connection.prepareStatement(sql);
int i = 1;
for (Object value : st.values()) {
ps.setObject(i++, value); // parameters set safely via JDBC
}
return ps;
};
// Usage (Java 21 preview syntax):
// String userId = getInput(); // could be "'; DROP TABLE users; --"
// PreparedStatement ps = SQL."SELECT * FROM users WHERE id = \{userId}";
// ps.executeQuery(); // SQL injection-safe!
// HTML-escaping processor
StringTemplate.Processor<String, RuntimeException> HTML = st -> {
StringBuilder sb = new StringBuilder();
Iterator<String> frags = st.fragments().iterator();
for (Object value : st.values()) {
sb.append(frags.next());
sb.append(escape(value.toString())); // HTML-escape each interpolated value
}
sb.append(frags.next());
return sb.toString();
};
// String escape(String s) — replaces < > & " ' with HTML entities
// RAW processor — get access to the StringTemplate object
StringTemplate template = RAW."Hello, \{name}!";
List<String> fragments = template.fragments(); // ["Hello, ", "!"]
List<Object> values = template.values(); // [name]
Stable Alternatives: formatted() and MessageFormat
Because string templates are still in flux, formatted()MessageFormatformatted()String.format(this, args)MessageFormat{0}{1}
// formatted() — Java 15+, instance method on String, same as String.format()
String name = "Bob";
int score = 92;
// Clean and stable
String msg1 = "Name: %s, Score: %d".formatted(name, score);
// Multi-line with text block + formatted (stable combo)
String json = """
{
"name": "%s",
"score": %d
}
""".formatted(name, score);
// String.format() — traditional, always works
String msg2 = String.format("Name: %s, Score: %d", name, score);
// StringBuilder — explicit but fast
String msg3 = new StringBuilder()
.append("Name: ").append(name)
.append(", Score: ").append(score)
.toString();
// String + concatenation — compiler converts to StringBuilder internally
// Fine for simple cases, not for loops
String msg4 = "Name: " + name + ", Score: " + score;
// MessageFormat — handles plurals, locale, choice format
String pattern = "You have {0} {0,choice,0#messages|1#message|1<messages}.";
System.out.println(MessageFormat.format(pattern, 0)); // You have 0 messages.
System.out.println(MessageFormat.format(pattern, 1)); // You have 1 message.
System.out.println(MessageFormat.format(pattern, 5)); // You have 5 messages.
formatted()Practical Patterns
For SQL, always use PreparedStatementformatted()String.format()
// Pattern 1: Build a JSON payload with text block + formatted (production-safe)
String buildUserJson(String name, int age, String email) {
return """
{
"name": "%s",
"age": %d,
"email": "%s"
}
""".formatted(
name.replace("\"", "\\\""), // escape quotes in name
age,
email.replace("\"", "\\\"")
);
}
// Pattern 2: Multi-line SQL with named parameters (using simple replacement)
String buildQuery(String table, int limit) {
return """
SELECT *
FROM %s
WHERE active = true
LIMIT %d
""".formatted(table, limit);
// Use PreparedStatement parameters for user input — never string format for values!
}
// Pattern 3: Email template
String buildEmail(String recipientName, String orderId, double total) {
return """
Dear %s,
Your order #%s has been confirmed.
Order total: $%.2f
Thank you for shopping with us!
""".formatted(recipientName, orderId, total);
}
// Pattern 4: Log message building — don't format if level is disabled
private static final Logger log = Logger.getLogger(...);
// BAD: "Hello " + heavyCompute() — always evaluates even if log.FINE is off
// GOOD: use lambda supplier with isLoggable check
log.finest(() -> "State: " + computeExpensiveState());