Contents
- Text Blocks — Java 15+
- String Templates Preview — Java 21
- Custom Template Processors
- Stable Alternatives — formatted() and MessageFormat
- Practical Patterns
Text blocks use triple-quote delimiters (""") and must start content on a new line after the opening delimiter. Indentation is normalized: the compiler determines the common leading whitespace from the content lines and the position of the closing """, then strips that many spaces from every line — so code indentation does not become string content. Trailing whitespace on each line is removed unless you use the \s escape to explicitly preserve it. Use \ at the end of a line to suppress the newline and join lines visually without inserting a newline in the result.
// 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\"
""";
The STR template processor (JEP 430, preview in Java 21) used \{expression} syntax for inline interpolation. The RAW processor gave access to the StringTemplate object — the fragments and values — before processing. The FMT processor combined interpolation with printf-style format specifiers. However, string templates were redesigned after their preview period; the STR and FMT processors were removed in Java 23. Do not use them in production code without understanding the stability implications.
// 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)
String Templates (STR, FMT) are a preview feature that requires --enable-preview. The feature was withdrawn after Java 22 for redesign. Do not use in production code without understanding the implications. Use formatted() or String.format() for stable interpolation.
A template processor implements StringTemplate.Processor and receives a StringTemplate that exposes two lists: fragments() (the literal string parts between interpolations) and values() (the interpolated expressions). The processor combines them however it wants — it can HTML-escape each interpolated value, validate that SQL identifiers are safe, or route values to JDBC prepared-statement parameters. This separation of literal structure from interpolated values is what makes template processors safe: the processor, not the caller, controls how values are embedded.
// 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]
Because string templates are still in flux, formatted() and MessageFormat are the stable production alternatives. formatted() is an instance method (Java 15+) equivalent to String.format(this, args) — use it with a text block for a clean multi-line template. MessageFormat uses numbered placeholders {0}, {1} and supports locale-sensitive formatting including plural choice patterns. Both are finalized and stable across all supported Java versions.
// 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.
For stable, production-safe string interpolation, use text blocks (Java 15+) combined with formatted(). This combination provides multi-line templates with values plugged in, without any preview feature risk.
For SQL, always use PreparedStatement parameters for user-supplied values — never string concatenation or format substitution for values (only for table/column names, and only from trusted sources). For HTML, use a templating library that escapes values. For log messages and diagnostic output, formatted() or String.format() are straightforward and safe. Await a stable string templates release before adopting template processors in production code.
// 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());