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 (""") 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.

Java
// 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 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.

Java
// 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.

Custom Template Processors

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.

Java
// 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() 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.

Java
// 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.

Practical Patterns

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.

Java
// 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());