Mastering Interpolated Strings with STR.”…” in Java

Java 21 introduces String Templates as a preview feature (JEP 430), bringing interpolated string capabilities to Java. This article provides a comprehensive guide to using the STR."..." processor and other string template features.

Understanding String Templates

String Templates combine literal text with embedded expressions and template processors to produce specialized results. The STR processor is the standard processor for string interpolation.

Basic String Interpolation

Step 1: Basic Syntax and Usage

public class BasicStringTemplates {
public static void main(String[] args) {
String name = "John";
int age = 30;
double salary = 55000.75;
// Traditional concatenation
String traditional = "Name: " + name + ", Age: " + age + ", Salary: " + salary;
// String template interpolation
String interpolated = STR."Name: \{name}, Age: \{age}, Salary: \{salary}";
System.out.println("Traditional: " + traditional);
System.out.println("Interpolated: " + interpolated);
// Expressions within templates
int x = 10, y = 20;
String calculation = STR."\{x} + \{y} = \{x + y}";
System.out.println(calculation); // "10 + 20 = 30"
// Method calls in templates
String methodCall = STR."Current time: \{java.time.LocalTime.now()}";
System.out.println(methodCall);
// Object method calls
Person person = new Person("Alice", 25);
String objectInfo = STR."Person: \{person.getName()}, Age: \{person.getAge()}";
System.out.println(objectInfo);
}
static class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
}
}

Step 2: Multi-line Templates

public class MultilineTemplates {
public record User(String username, String email, String role, 
LocalDateTime createdAt) {}
public static void main(String[] args) {
User user = new User("johndoe", "[email protected]", "ADMIN", 
LocalDateTime.now());
// Multi-line string template
String userProfile = STR."""
User Profile:
============
Username: \{user.username()}
Email:    \{user.email()}
Role:     \{user.role()}
Created:  \{user.createdAt().format(
java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME)}
Last Login: \{LocalDateTime.now().minusHours(2)}
Status:     Active
""";
System.out.println(userProfile);
// SQL query template
String tableName = "users";
int limit = 100;
String sqlQuery = STR."""
SELECT 
id, 
username, 
email,
created_at
FROM \{tableName}
WHERE status = 'ACTIVE'
ORDER BY created_at DESC
LIMIT \{limit}
""";
System.out.println("\nSQL Query:\n" + sqlQuery);
// JSON template
String jsonResponse = STR."""
{
"status": "success",
"data": {
"user": {
"username": "\{user.username()}",
"email": "\{user.email()}",
"role": "\{user.role()}"
}
},
"timestamp": "\{Instant.now()}"
}
""";
System.out.println("\nJSON Response:\n" + jsonResponse);
}
}

Advanced Template Features

Step 3: Complex Expressions and Formatting

import java.math.BigDecimal;
import java.text.NumberFormat;
import java.util.*;
public class AdvancedTemplates {
public static void main(String[] args) {
// Complex expressions
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
String stats = STR."""
List Analysis:
- List: \{numbers}
- Size: \{numbers.size()}
- Sum: \{numbers.stream().mapToInt(Integer::intValue).sum()}
- Average: \{numbers.stream().mapToInt(Integer::intValue).average().orElse(0)}
- Max: \{Collections.max(numbers)}
- Min: \{Collections.min(numbers)}
""";
System.out.println(stats);
// Conditional expressions
int score = 85;
String gradeResult = STR."""
Score: \{score}
Grade: \{
score >= 90 ? "A" :
score >= 80 ? "B" :
score >= 70 ? "C" :
score >= 60 ? "D" : "F"
}
Status: \{score >= 70 ? "PASS" : "FAIL"}
""";
System.out.println(gradeResult);
// Method chain expressions
String text = "   Hello, World!   ";
String processed = STR."""
Original: '\{text}'
Trimmed: '\{text.trim()}'
Upper Case: '\{text.trim().toUpperCase()}'
Lower Case: '\{text.trim().toLowerCase()}'
Length: \{text.trim().length()}
""";
System.out.println(processed);
// Date formatting
LocalDate today = LocalDate.now();
String dateInfo = STR."""
Date Information:
- ISO: \{today}
- Formatted: \{today.format(DateTimeFormatter.ofPattern("MMM dd, yyyy"))}
- Day of Week: \{today.getDayOfWeek()}
- Month: \{today.getMonth()}
- Year: \{today.getYear()}
- Is Leap Year: \{today.isLeapYear()}
""";
System.out.println(dateInfo);
}
// Financial formatting example
public static String generateInvoice(Invoice invoice) {
NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.US);
return STR."""
INVOICE #\{invoice.invoiceNumber()}
========================
Customer: \{invoice.customerName()}
Date: \{invoice.date()}
Items:
\{
invoice.items().stream()
.map(item -> STR."  • \{item.description()}: \{currency.format(item.amount())}")
.collect(java.util.stream.Collectors.joining("\n"))
}
Subtotal: \{currency.format(invoice.subtotal())}
Tax: \{currency.format(invoice.tax())}
Total: \{currency.format(invoice.total())}
Thank you for your business!
""";
}
record Invoice(String invoiceNumber, String customerName, LocalDate date,
List<LineItem> items, double subtotal, double tax, double total) {}
record LineItem(String description, double amount) {}
}

Custom Template Processors

Step 4: Beyond STR - Creating Custom Processors

import java.time.LocalDateTime;
import java.util.Locale;
import java.util.regex.Pattern;
public class CustomTemplateProcessors {
// Custom processor for SQL safe interpolation
public static final StringTemplate.Processor<String, RuntimeException> SQL = 
StringTemplate.Processor.of((StringTemplate st) -> {
StringBuilder sb = new StringBuilder();
List<String> fragments = st.fragments();
List<Object> values = st.values();
for (int i = 0; i < fragments.size(); i++) {
sb.append(fragments.get(i));
if (i < values.size()) {
Object value = values.get(i);
// Basic SQL injection prevention
if (value instanceof String str) {
// Escape single quotes for SQL
String escaped = str.replace("'", "''");
sb.append("'").append(escaped).append("'");
} else if (value instanceof Number) {
sb.append(value);
} else if (value == null) {
sb.append("NULL");
} else {
sb.append("'").append(value.toString().replace("'", "''")).append("'");
}
}
}
return sb.toString();
});
// JSON processor with proper escaping
public static final StringTemplate.Processor<String, RuntimeException> JSON = 
StringTemplate.Processor.of((StringTemplate st) -> {
StringBuilder sb = new StringBuilder();
List<String> fragments = st.fragments();
List<Object> values = st.values();
for (int i = 0; i < fragments.size(); i++) {
sb.append(fragments.get(i));
if (i < values.size()) {
Object value = values.get(i);
if (value instanceof String str) {
// JSON escape
String escaped = str.replace("\"", "\\\"")
.replace("\\", "\\\\")
.replace("\b", "\\b")
.replace("\f", "\\f")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
sb.append("\"").append(escaped).append("\"");
} else if (value instanceof Number || value instanceof Boolean) {
sb.append(value);
} else if (value == null) {
sb.append("null");
} else {
sb.append("\"").append(value.toString()
.replace("\"", "\\\"")
.replace("\\", "\\\\"))
.append("\"");
}
}
}
return sb.toString();
});
// HTML processor with basic XSS protection
public static final StringTemplate.Processor<String, RuntimeException> HTML = 
StringTemplate.Processor.of((StringTemplate st) -> {
StringBuilder sb = new StringBuilder();
List<String> fragments = st.fragments();
List<Object> values = st.values();
for (int i = 0; i < fragments.size(); i++) {
sb.append(fragments.get(i));
if (i < values.size()) {
Object value = values.get(i);
if (value instanceof String str) {
// Basic HTML escaping
String escaped = str.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#x27;");
sb.append(escaped);
} else {
sb.append(value);
}
}
}
return sb.toString();
});
public static void main(String[] args) {
// Using SQL processor
String tableName = "users";
String username = "john'; DROP TABLE users; --";
int age = 30;
String sql = SQL."""
SELECT * FROM \{tableName} 
WHERE username = \{username} 
AND age > \{age}
""";
System.out.println("SQL Query:\n" + sql);
// Using JSON processor
String name = "John\"Doe";
String email = "[email protected]";
boolean active = true;
String json = JSON."""
{
"name": "\{name}",
"email": "\{email}",
"active": \{active},
"createdAt": "\{LocalDateTime.now()}"
}
""";
System.out.println("\nJSON:\n" + json);
// Using HTML processor
String userInput = "<script>alert('XSS')</script>";
String title = "User Profile";
String html = HTML."""
<div class="profile">
<h1>\{title}</h1>
<p>User input: \{userInput}</p>
<span>Generated at: \{LocalDateTime.now()}</span>
</div>
""";
System.out.println("\nHTML:\n" + html);
}
}

Real-World Use Cases

Step 5: Practical Applications

import java.util.*;
public class RealWorldTemplates {
// Email template system
public static class EmailTemplates {
public static String welcomeEmail(String userName, String activationLink) {
return STR."""
Subject: Welcome to Our Service, \{userName}!
Dear \{userName},
Welcome to Our Service! We're excited to have you on board.
To get started, please activate your account by clicking the link below:
\{activationLink}
This link will expire in 24 hours.
If you have any questions, please don't hesitate to contact our support team.
Best regards,
The Our Service Team
Generated: \{LocalDateTime.now()}
""";
}
public static String passwordReset(String userName, String resetLink) {
return STR."""
Subject: Password Reset Request
Hello \{userName},
We received a request to reset your password. If you didn't make this request, 
please ignore this email.
To reset your password, click the link below:
\{resetLink}
This link will expire in 1 hour.
For security reasons, please do not share this link with anyone.
Best regards,
Security Team
""";
}
}
// Logging system with templates
public static class TemplateLogger {
private static final StringTemplate.Processor<String, RuntimeException> LOG =
StringTemplate.Processor.of((StringTemplate st) -> {
String timestamp = LocalDateTime.now().format(
java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME);
String message = STR.process(st);
return STR."[\{timestamp}] \{message}";
});
public static void info(StringTemplate st) {
System.out.println(LOG."INFO: \{st}");
}
public static void error(StringTemplate st) {
System.err.println(LOG."ERROR: \{st}");
}
public static void debug(StringTemplate st) {
System.out.println(LOG."DEBUG: \{st}");
}
}
// Configuration template system
public static class ConfigTemplates {
public static String generateDockerfile(String baseImage, int port, 
String maintainer, List<String> dependencies) {
return STR."""
# Generated Dockerfile
FROM \{baseImage}
LABEL maintainer="\{maintainer}"
LABEL version="1.0"
LABEL description="Application container"
# Install dependencies
RUN apt-get update && apt-get install -y \
\{
String.join(" \\\n    ", dependencies)
}
# Expose application port
EXPOSE \{port}
# Set working directory
WORKDIR /app
# Copy application files
COPY . .
# Start application
CMD ["java", "-jar", "app.jar"]
# Health check
HEALTHCHECK --interval=30s --timeout=3s \\
CMD curl -f http://localhost:\{port}/health || exit 1
""";
}
public static String generateApplicationProperties(String dbUrl, String dbUsername, 
int serverPort, boolean debugMode) {
return STR."""
# Application Properties
# Generated: \{LocalDateTime.now()}
# Database Configuration
spring.datasource.url=\{dbUrl}
spring.datasource.username=\{dbUsername}
spring.datasource.password=$\{DB_PASSWORD}
# Server Configuration
server.port=\{serverPort}
server.servlet.context-path=/
# Logging
logging.level.com.example=\{debugMode ? "DEBUG" : "INFO"}
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
# Security
security.jwt.secret=$\{JWT_SECRET}
security.jwt.expiration=86400
# Feature Flags
feature.email.notifications=true
feature.analytics.enabled=\{debugMode ? "false" : "true"}
""";
}
}
public static void main(String[] args) {
// Test email templates
String welcomeEmail = EmailTemplates.welcomeEmail(
"John Doe", 
"https://example.com/activate?token=abc123"
);
System.out.println("Welcome Email:\n" + welcomeEmail);
// Test logging
String userId = "user123";
int actionCount = 5;
TemplateLogger.info(STR."User \{userId} performed \{actionCount} actions");
TemplateLogger.error(STR."Database connection failed for user \{userId}");
// Test configuration generation
List<String> dependencies = Arrays.asList(
"openjdk-17-jdk", "curl", "vim", "git"
);
String dockerfile = ConfigTemplates.generateDockerfile(
"openjdk:17", 8080, "[email protected]", dependencies
);
System.out.println("\nDockerfile:\n" + dockerfile);
String appProps = ConfigTemplates.generateApplicationProperties(
"jdbc:mysql://localhost:3306/mydb", "admin", 8080, true
);
System.out.println("\nApplication Properties:\n" + appProps);
}
}

Performance Considerations

Step 6: Efficiency and Best Practices

public class PerformanceTemplates {
// Benchmark comparison
public static void performanceComparison() {
String name = "John";
int age = 30;
double salary = 55000.75;
int iterations = 100_000;
// Traditional concatenation
long startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
String result = "Name: " + name + ", Age: " + age + ", Salary: " + salary;
}
long concatTime = System.nanoTime() - startTime;
// StringBuilder
startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
String result = new StringBuilder()
.append("Name: ").append(name)
.append(", Age: ").append(age)
.append(", Salary: ").append(salary)
.toString();
}
long builderTime = System.nanoTime() - startTime;
// String template
startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
String result = STR."Name: \{name}, Age: \{age}, Salary: \{salary}";
}
long templateTime = System.nanoTime() - startTime;
// String.format
startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
String result = String.format("Name: %s, Age: %d, Salary: %.2f", name, age, salary);
}
long formatTime = System.nanoTime() - startTime;
System.out.println("Performance Comparison (" + iterations + " iterations):");
System.out.printf("Concatenation:  %10d ns%n", concatTime);
System.out.printf("StringBuilder:  %10d ns%n", builderTime);
System.out.printf("String Template:%10d ns%n", templateTime);
System.out.printf("String.format:  %10d ns%n", formatTime);
}
// Best practices for template usage
public static class TemplateBestPractices {
// Good: Simple expressions
public static String createUserMessage(String name, int messages) {
return STR."Hello \{name}, you have \{messages} new messages";
}
// Avoid: Complex logic in templates
public static String createComplexReport(List<Double> data) {
// Instead of putting complex logic in template...
double average = data.stream().mapToDouble(Double::doubleValue).average().orElse(0);
double max = Collections.max(data);
double min = Collections.min(data);
// ...precompute values and use in template
return STR."""
Data Report:
- Count: \{data.size()}
- Average: \{String.format("%.2f", average)}
- Range: \{String.format("%.2f", min)} to \{String.format("%.2f", max)}
""";
}
// Good: Use with records
public record Product(String id, String name, double price, int stock) {}
public static String productInfo(Product product) {
return STR."""
Product: \{product.name()}
ID: \{product.id()}
Price: $\{String.format("%.2f", product.price())}
Stock: \{product.stock()}
""";
}
// Template reuse pattern
private static final StringTemplate.Processor<String, RuntimeException> ERROR_TEMPLATE =
StringTemplate.Processor.of(st -> {
String timestamp = LocalDateTime.now().toString();
String message = STR.process(st);
return STR."[\{timestamp}] ERROR: \{message}";
});
public static void logError(String errorType, String details, String userId) {
String errorMessage = ERROR_TEMPLATE."""
Type: \{errorType}
User: \{userId}
Details: \{details}
""";
System.err.println(errorMessage);
}
}
public static void main(String[] args) {
// Run performance comparison
performanceComparison();
// Demonstrate best practices
String userMessage = TemplateBestPractices.createUserMessage("Alice", 5);
System.out.println("\nUser Message: " + userMessage);
List<Double> data = List.of(10.5, 20.3, 15.7, 8.2, 25.1);
String report = TemplateBestPractices.createComplexReport(data);
System.out.println("\nReport:\n" + report);
TemplateBestPractices.Product product = new TemplateBestPractices.Product(
"P123", "Laptop", 999.99, 10
);
String productInfo = TemplateBestPractices.productInfo(product);
System.out.println("\nProduct Info:\n" + productInfo);
TemplateBestPractices.logError("Database", "Connection timeout", "user456");
}
}

Integration with Existing Code

Step 7: Migrating Existing Code to Templates

public class MigrationExamples {
// Before: Traditional approaches
public static class TraditionalApproach {
public static String buildUserInfo(String firstName, String lastName, 
int age, String city) {
return "User: " + firstName + " " + lastName + 
", Age: " + age + 
", City: " + city;
}
public static String buildSqlQuery(String table, String whereClause, 
int limit, String orderBy) {
return "SELECT * FROM " + table + 
" WHERE " + whereClause + 
" ORDER BY " + orderBy + 
" LIMIT " + limit;
}
public static String buildHtmlElement(String tag, String content, 
String... attributes) {
StringBuilder sb = new StringBuilder();
sb.append("<").append(tag);
for (int i = 0; i < attributes.length; i += 2) {
if (i + 1 < attributes.length) {
sb.append(" ").append(attributes[i])
.append("=\"").append(attributes[i + 1]).append("\"");
}
}
sb.append(">").append(content).append("</").append(tag).append(">");
return sb.toString();
}
}
// After: Using string templates
public static class TemplateApproach {
public static String buildUserInfo(String firstName, String lastName, 
int age, String city) {
return STR."User: \{firstName} \{lastName}, Age: \{age}, City: \{city}";
}
public static String buildSqlQuery(String table, String whereClause, 
int limit, String orderBy) {
return STR."SELECT * FROM \{table} WHERE \{whereClause} ORDER BY \{orderBy} LIMIT \{limit}";
}
public static String buildHtmlElement(String tag, String content, 
String... attributes) {
StringBuilder attrs = new StringBuilder();
for (int i = 0; i < attributes.length; i += 2) {
if (i + 1 < attributes.length) {
attrs.append(" ")
.append(attributes[i])
.append("=\"")
.append(attributes[i + 1])
.append("\"");
}
}
return STR."<\{tag}\{attrs}>\{content}</\{tag}>";
}
}
// Gradual migration example
public static class HybridApproach {
public static String generateReport(Map<String, Object> data) {
// Use template for the main structure
String header = STR."""
==========================
REPORT: \{data.get("reportName")}
Generated: \{LocalDateTime.now()}
==========================
""";
// Use traditional approach for dynamic sections
StringBuilder body = new StringBuilder();
for (Map.Entry<String, Object> entry : data.entrySet()) {
if (!entry.getKey().equals("reportName")) {
body.append(entry.getKey())
.append(": ")
.append(entry.getValue())
.append("\n");
}
}
// Combine both approaches
return STR."""
\{header}
\{body.toString()}
==========================
""";
}
}
public static void main(String[] args) {
// Compare approaches
String traditional = TraditionalApproach.buildUserInfo("John", "Doe", 30, "New York");
String template = TemplateApproach.buildUserInfo("John", "Doe", 30, "New York");
System.out.println("Traditional: " + traditional);
System.out.println("Template:    " + template);
// HTML generation comparison
String htmlTraditional = TraditionalApproach.buildHtmlElement(
"div", "Hello World", "class", "container", "id", "main"
);
String htmlTemplate = TemplateApproach.buildHtmlElement(
"div", "Hello World", "class", "container", "id", "main"
);
System.out.println("\nHTML Traditional: " + htmlTraditional);
System.out.println("HTML Template:    " + htmlTemplate);
// Hybrid approach
Map<String, Object> reportData = Map.of(
"reportName", "Monthly Sales",
"totalSales", 150000.0,
"unitsSold", 1250,
"topProduct", "Widget X",
"growth", "+15.5%"
);
String hybridReport = HybridApproach.generateReport(reportData);
System.out.println("\nHybrid Report:\n" + hybridReport);
}
}

Key Benefits and Best Practices

Benefits:

  1. Readability: Much cleaner than concatenation or StringBuilder
  2. Safety: Compile-time checking of expression syntax
  3. Performance: Generally faster than String.format()
  4. Flexibility: Custom processors for different contexts
  5. Maintainability: Easier to modify and understand

Best Practices:

  1. Use for simple expressions and avoid complex logic in templates
  2. Create custom processors for domain-specific formatting
  3. Precompute complex values before using in templates
  4. Use multi-line templates for complex structured text
  5. Consider performance for hot code paths
  6. Combine with records for type-safe data interpolation

String templates with STR."..." provide a powerful, readable, and efficient way to handle string interpolation in Java, significantly improving upon traditional concatenation approaches while maintaining type safety and performance.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper