Java 25 — String Templates & Flexible Constructor Bodies
Java 25 finalizes string templates with the STR processor and flexible constructor bodies — two features that simplify everyday Java code.
Java 25 finalizes two features that affect how you write everyday code: string templates (JEP 487) and flexible constructor bodies (JEP 492). Both were preview features in earlier releases. Now they’re stable.
String templates
String concatenation in Java has always been awkward:
// Concatenation — hard to read
String msg = "Hello, " + user.name() + "! You have " + count + " messages.";
// String.format — verbose, positional args are error-prone
String msg = String.format("Hello, %s! You have %d messages.", user.name(), count);
// MessageFormat — even worse
String msg = MessageFormat.format("Hello, {0}! You have {1} messages.", user.name(), count);
String templates fix this with embedded expressions:
String name = "Alice";
int count = 5;
String msg = STR."Hello, \{name}! You have \{count} messages.";
// "Hello, Alice! You have 5 messages."
The \{expression} syntax embeds any Java expression directly in the string. The STR processor handles interpolation.
Expressions in templates
You can embed any expression, not just variables:
int x = 10;
int y = 20;
String result = STR."\{x} + \{y} = \{x + y}";
// "10 + 20 = 30"
String greeting = STR."Hello, \{user.name().toUpperCase()}!";
// "Hello, ALICE!"
String status = STR."Status: \{isActive ? "active" : "inactive"}";
// "Status: active"
Multi-line templates
String templates work with text blocks:
String html = STR."""
<html>
<body>
<h1>Hello, \{name}</h1>
<p>You have \{count} unread messages.</p>
</body>
</html>
""";
The indentation rules are the same as regular text blocks.
Method calls in templates
record Product(String name, double price) {}
Product product = new Product("Widget", 29.99);
String line = STR."\{product.name()} — $\{String.format("%.2f", product.price())}";
// "Widget — $29.99"
Template expressions are not just strings
The template expression STR."..." is processed by the STR template processor, which returns a String. But the template system is designed to support custom processors that return any type.
// STR returns String
String s = STR."Hello, \{name}";
// FMT returns String with format specifiers
String formatted = FMT."%-10s\{name} %5d\{count}";
The FMT processor supports printf-style format specifiers before the embedded expression.
Custom template processors
You can build your own processor. A common use case is SQL query building:
import java.lang.StringTemplate;
StringTemplate.Processor<PreparedStatement, SQLException> SQL = template -> {
StringBuilder query = new StringBuilder();
List<Object> params = new ArrayList<>();
Iterator<String> fragments = template.fragments().iterator();
Iterator<Object> values = template.values().iterator();
query.append(fragments.next());
while (fragments.hasNext()) {
query.append("?");
params.add(values.next());
query.append(fragments.next());
}
PreparedStatement ps = connection.prepareStatement(query.toString());
for (int i = 0; i < params.size(); i++) {
ps.setObject(i + 1, params.get(i));
}
return ps;
};
Usage:
String name = "Alice";
int minAge = 18;
PreparedStatement ps = SQL."SELECT * FROM users WHERE name = \{name} AND age >= \{minAge}";
This produces a parameterized query — no SQL injection possible. The processor separates the template fragments from the values and uses placeholders.
Why not just string interpolation?
Many languages have string interpolation (Kotlin’s "$variable", Python’s f"{variable}"). Java’s approach is different — it’s processor-based. The template is an object that a processor transforms. This enables:
- SQL safety — processors can create parameterized queries
- HTML safety — processors can escape user input
- JSON safety — processors can produce valid JSON
- Validation — processors can validate at compile time or runtime
Simple string interpolation can’t do this. It always produces a string, and that string might contain injection vulnerabilities.
Flexible constructor bodies
Before Java 25, constructor code before super() or this() was limited. You couldn’t execute statements before the delegation call:
// Before Java 25 — this does NOT compile
class PositiveInteger {
private final int value;
PositiveInteger(int value) {
if (value <= 0) {
throw new IllegalArgumentException("Value must be positive: " + value);
}
this.value = value; // ERROR: must call super() first
}
}
Wait — actually that example does compile because there’s no explicit super(). The real problem is when you need to validate or transform arguments before calling super() or this():
// Before Java 25 — this does NOT compile
class NamedLogger extends Logger {
NamedLogger(String rawName) {
String normalized = rawName.strip().toLowerCase();
if (normalized.isEmpty()) {
throw new IllegalArgumentException("Name cannot be blank");
}
super(normalized); // ERROR: super() must be first statement
}
}
The workaround was ugly
// Before Java 25 — static helper method workaround
class NamedLogger extends Logger {
NamedLogger(String rawName) {
super(validateAndNormalize(rawName));
}
private static String validateAndNormalize(String rawName) {
String normalized = rawName.strip().toLowerCase();
if (normalized.isEmpty()) {
throw new IllegalArgumentException("Name cannot be blank");
}
return normalized;
}
}
Java 25 — statements before super()
Now you can run code before super() or this(), as long as you don’t access this:
class NamedLogger extends Logger {
NamedLogger(String rawName) {
String normalized = rawName.strip().toLowerCase();
if (normalized.isEmpty()) {
throw new IllegalArgumentException("Name cannot be blank");
}
super(normalized);
}
}
Validation before delegation
class BoundedList<T> extends ArrayList<T> {
private final int maxSize;
BoundedList(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("Max size must be positive: " + maxSize);
}
super();
this.maxSize = maxSize;
}
}
Transformation before this()
Works with this() too:
record Range(int start, int end) {
Range(int start, int end) {
if (start > end) {
// Swap to ensure start <= end
int temp = start;
start = end;
end = temp;
}
this.start = start;
this.end = end;
}
}
What you still can’t do
You can’t access this before super(). These are compile errors:
class Derived extends Base {
Derived(int value) {
this.field = value; // ERROR: can't access this before super()
this.method(); // ERROR: can't access this before super()
super(value);
}
}
This restriction exists because the object isn’t fully constructed until super() completes. The new feature lets you prepare arguments and validate inputs — it doesn’t let you use the half-constructed object.
Practical example: record validation
Records benefit heavily from flexible constructor bodies:
record EmailAddress(String value) {
EmailAddress(String value) {
value = value.strip().toLowerCase();
if (!value.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")) {
throw new IllegalArgumentException("Invalid email: " + value);
}
this.value = value;
}
}
Before Java 25, you needed a compact constructor or a static factory method. Now the canonical constructor can do validation and normalization inline.
Practical example: builder-style constructor chaining
class HttpClient {
private final String baseUrl;
private final int timeout;
private final boolean followRedirects;
HttpClient(String baseUrl) {
// Validate and normalize before delegating
if (baseUrl == null || baseUrl.isBlank()) {
throw new IllegalArgumentException("Base URL required");
}
String normalized = baseUrl.endsWith("/")
? baseUrl.substring(0, baseUrl.length() - 1)
: baseUrl;
this(normalized, 30_000, true);
}
HttpClient(String baseUrl, int timeout, boolean followRedirects) {
this.baseUrl = baseUrl;
this.timeout = timeout;
this.followRedirects = followRedirects;
}
}
Summary
String templates give Java safe, readable string interpolation with the processor model enabling SQL-safe queries, HTML-safe output, and custom transformations.
Flexible constructor bodies remove the “super() must be first statement” restriction, eliminating static helper methods for argument validation and transformation.
Both features reduce boilerplate without sacrificing safety — a running theme in modern Java.