Java 17 Text Blocks

1. Introduction

If you have been writing Java for any length of time, you know the pain of constructing multi-line strings. Every JSON payload, every SQL query, every HTML snippet you needed to embed in Java code turned into a war against escape characters, concatenation operators, and broken indentation. A simple 5-line JSON object became a 10-line mess of "{", + "\n", and backslash escapes that nobody wanted to read or maintain.

Consider what it took to embed a simple JSON string before text blocks existed:

// The old way: multi-line strings were painful
String json = "{\n" +
              "  \"name\": \"John Doe\",\n" +
              "  \"age\": 30,\n" +
              "  \"email\": \"john@example.com\",\n" +
              "  \"address\": {\n" +
              "    \"street\": \"123 Main St\",\n" +
              "    \"city\": \"Springfield\"\n" +
              "  }\n" +
              "}";

Count the problems: escaped double quotes everywhere, explicit \n newline characters, concatenation operators on every line, and indentation that has nothing to do with the actual JSON structure. If you needed to change a field, you had to navigate a minefield of escape sequences.

Text blocks solve all of this. They were introduced as a preview feature in Java 13 (JEP 355), refined in Java 14 (JEP 368), and finalized as a permanent feature in Java 15 (JEP 378). In Java 17 — the current long-term support (LTS) release — text blocks are a stable, production-ready tool that every Java developer should know.

A text block is a multi-line string literal that lets you write strings exactly as they appear, without escape characters for newlines or double quotes. The same JSON from above becomes:

// With text blocks: clean, readable, maintainable
String json = """
        {
          "name": "John Doe",
          "age": 30,
          "email": "john@example.com",
          "address": {
            "street": "123 Main St",
            "city": "Springfield"
          }
        }
        """;

No escape characters. No concatenation. The string looks exactly like the JSON it represents. When you need to modify it, you edit it like you would edit a regular JSON file. This is what text blocks bring to the table.

Key benefits of text blocks:

  • Readability — Multi-line strings look like the content they represent
  • No escape characters — Double quotes and newlines work naturally
  • Smart indentation — Java automatically strips common leading whitespace
  • Still a String — Text blocks produce regular java.lang.String objects, fully compatible with all existing String methods
  • Compile-time feature — Zero runtime overhead; the compiler processes text blocks during compilation

2. Basic Syntax

A text block starts with three double-quote characters ("""), followed by optional whitespace and a mandatory line terminator (newline). The content of the text block starts on the next line. The text block ends with another """.

The rules are simple but strict:

  • The opening """ must be followed by a line terminator — you cannot put content on the same line as the opening delimiter
  • The closing """ can be on its own line or at the end of the last content line
  • Everything between the opening line terminator and the closing """ is the content
  • A newline in the source code is a newline in the string
public class TextBlockBasics {
    public static void main(String[] args) {

        // Basic text block
        String greeting = """
                Hello, World!
                Welcome to Java Text Blocks.
                This is a multi-line string.
                """;
        System.out.println(greeting);
        // Output:
        // Hello, World!
        // Welcome to Java Text Blocks.
        // This is a multi-line string.

        // Single line in a text block (legal but unusual)
        String single = """
                Just one line
                """;
        System.out.println(single); // "Just one line\n"

        // Empty text block
        String empty = """
                """;
        System.out.println(empty.isEmpty()); // true (only contains "")
        // Actually empty is just "\n" -- NOT empty!

        // Truly empty-ish text block
        String minimal = """
\
""";
        System.out.println(minimal.isEmpty()); // true (line continuation removes the newline)
    }
}

Important: the opening delimiter rule. The following code will NOT compile because content appears on the same line as the opening """:

// COMPILE ERROR: illegal text block open delimiter sequence
String bad = """Hello""";

// COMPILE ERROR: content on same line as opening delimiter
String alsoBad = """Hello
        World""";

// CORRECT: content starts on the next line
String good = """
        Hello
        World""";

Closing delimiter position matters. Where you place the closing """ affects whether the string ends with a newline:

public class ClosingDelimiterDemo {
    public static void main(String[] args) {

        // Closing """ on its own line: string ENDS with a newline
        String withNewline = """
                Hello
                """;
        System.out.println(withNewline.endsWith("\n")); // true
        // Value: "Hello\n"

        // Closing """ on the last content line: NO trailing newline
        String withoutNewline = """
                Hello""";
        System.out.println(withoutNewline.endsWith("\n")); // false
        // Value: "Hello"

        // This distinction matters when building output
        System.out.println("---");
        System.out.print(withNewline);    // prints "Hello" then newline
        System.out.print(withoutNewline); // prints "Hello" with no newline
        System.out.println("---");
    }
}

Double quotes inside text blocks. One of the biggest quality-of-life improvements is that double quotes no longer need escaping (in most cases):

public class QuotesInTextBlocks {
    public static void main(String[] args) {

        // Double quotes work without escaping
        String html = """
                

Hello, "World"!

"""; System.out.println(html); // You CAN still escape them if you want String escaped = """ He said, \"Hello!\" """; // Three consecutive double quotes must have at least one escaped String tricky = """ This contains \""" three quotes """; // Or break them up: String alsoWorks = """ This contains ""\" three quotes """; } }

3. Indentation Management

Indentation management is where text blocks truly shine, and where most developers get confused the first time they use them. The Java compiler uses a clever algorithm to separate incidental whitespace (indentation caused by your source code formatting) from essential whitespace (indentation that is actually part of the string content).

How it works: The compiler looks at every line of the text block, including the line containing the closing """, and finds the common leading whitespace — the smallest number of leading spaces shared by all non-blank lines. It then strips that many spaces from the beginning of every line. This process is called re-indentation.

Think of it like this: the compiler finds the leftmost non-space character across all lines and makes that column the “zero position.” Everything to its left is incidental and gets removed. Everything to its right is essential and stays.

public class IndentationDemo {
    public static void main(String[] args) {

        // All lines have 16 spaces of leading whitespace (incidental)
        // The compiler strips all 16, leaving no leading spaces
        String noIndent = """
                Line one
                Line two
                Line three
                """;
        // Result:
        // "Line one\nLine two\nLine three\n"

        // Some lines have extra indentation beyond the common baseline
        String withIndent = """
                Parent
                    Child 1
                    Child 2
                        Grandchild
                """;
        // Common leading whitespace: 16 spaces (from "Parent" and closing """)
        // Result:
        // "Parent\n    Child 1\n    Child 2\n        Grandchild\n"

        System.out.println(withIndent);
        // Output:
        // Parent
        //     Child 1
        //     Child 2
        //         Grandchild
    }
}

The Closing Delimiter Position Trick

Here is the key insight that trips up beginners: the closing """ participates in the common-whitespace calculation. By moving the closing delimiter left or right, you control how much indentation remains in the final string.

public class ClosingDelimiterIndent {
    public static void main(String[] args) {

        // Closing """ aligned with content: no indentation in result
        String noIndent = """
                SELECT *
                FROM users
                WHERE active = true
                """;
        System.out.println("--- No indent ---");
        System.out.println(noIndent);
        // SELECT *
        // FROM users
        // WHERE active = true

        // Closing """ moved LEFT of content: content is indented in result
        String indented = """
                SELECT *
                FROM users
                WHERE active = true
            """;
        // The closing """ has 12 spaces, content has 16 spaces
        // Common whitespace = 12, so 16-12 = 4 spaces of indent remain
        System.out.println("--- Indented ---");
        System.out.println(indented);
        //     SELECT *
        //     FROM users
        //     WHERE active = true

        // Closing """ at column zero: all source indentation preserved
        String fullyIndented = """
                SELECT *
                FROM users
                WHERE active = true
""";
        System.out.println("--- Fully indented ---");
        System.out.println(fullyIndented);
        //                 SELECT *
        //                 FROM users
        //                 WHERE active = true
    }
}

Visualization of incidental vs essential whitespace:

Scenario Closing """ Position Result
Aligned with content Same column as content No leading whitespace in output
Left of content Less indented than content Content retains relative indentation
Right of content More indented than content Same as aligned (closing delimiter does not add spaces)
Column zero No indentation at all All source indentation preserved

Blank Lines and Trailing Whitespace

Blank lines within a text block are preserved. However, trailing whitespace on each line is stripped by default. This is an important distinction — Java actively removes spaces and tabs at the end of each line during compilation. If you need trailing spaces, you must use the \s escape sequence (covered in the next section).

public class BlankLinesDemo {
    public static void main(String[] args) {

        // Blank lines are preserved
        String withBlanks = """
                Paragraph one.

                Paragraph two.

                Paragraph three.
                """;
        System.out.println(withBlanks);
        // Output:
        // Paragraph one.
        //
        // Paragraph two.
        //
        // Paragraph three.

        // Trailing whitespace is stripped!
        String trailing = """
                Hello
                World
                """;
        // The spaces after "Hello" and "World" are REMOVED
        System.out.println(trailing.contains("Hello     ")); // false
        System.out.println(trailing.contains("Hello"));       // true
    }
}

4. Escape Sequences

Text blocks support all traditional Java escape sequences (\n, \t, \\, \", etc.) plus two new ones that were introduced specifically for text blocks in Java 14:

Escape Name Purpose
\s Space escape Inserts a single space that is not stripped during trailing whitespace removal
\ (backslash at end of line) Line continuation Suppresses the newline character, joining the next line to the current one

The \s Escape: Preserving Trailing Spaces

Since text blocks strip trailing whitespace by default, the \s escape gives you a way to keep spaces when they matter. The \s translates to a single space character (U+0020), and its presence prevents the trailing-whitespace stripping algorithm from removing spaces on that line — because the line now ends with a non-whitespace escape sequence.

public class SpaceEscapeDemo {
    public static void main(String[] args) {

        // Without \s: trailing spaces are stripped
        String stripped = """
                Name:  John
                Age:   30
                """;
        System.out.println(stripped);
        // "Name:  John\nAge:   30\n" -- trailing spaces gone

        // With \s: trailing spaces are preserved
        String preserved = """
                Name:  John    \s
                Age:   30      \s
                """;
        System.out.println(preserved);
        // "Name:  John     \nAge:   30       \n" -- trailing spaces kept!

        // Useful for fixed-width formatting
        String table = """
                ID    Name          Status\s
                1     Alice         Active\s
                2     Bob           Inactive\s
                3     Charlie       Active\s
                """;
        System.out.println(table);
    }
}

The \ Line Continuation Escape

The backslash at the end of a line suppresses the newline, effectively joining the current line with the next one. This is invaluable when you have a very long line that you want to break in source code for readability, but you need it to be a single line in the actual string.

public class LineContinuationDemo {
    public static void main(String[] args) {

        // Without line continuation: each source line becomes a separate line
        String multiLine = """
                This is a very long sentence that \
                spans multiple lines in source code \
                but produces a single line in the output.""";
        System.out.println(multiLine);
        // "This is a very long sentence that spans multiple lines in source code but produces a single line in the output."

        // Practical use: long SQL query readable in source, single line at runtime
        String sql = """
                SELECT u.id, u.name, u.email, u.created_at, \
                u.updated_at, u.status, u.role \
                FROM users u \
                WHERE u.status = 'ACTIVE' \
                AND u.created_at > '2024-01-01'""";
        System.out.println(sql);
        // Single line: SELECT u.id, u.name, u.email, ...

        // Combining \s and \ for precise control
        String message = """
                Dear Customer,\s\
                your order #12345 has been shipped.\s\
                Expected delivery: 3-5 business days.""";
        System.out.println(message);
        // "Dear Customer, your order #12345 has been shipped. Expected delivery: 3-5 business days."
    }
}

Combining Traditional and New Escapes

Text blocks support all the classic escape sequences alongside the new ones:

public class AllEscapesDemo {
    public static void main(String[] args) {

        String allEscapes = """
                Tab here:\tafter tab
                Newline in middle:
                first\nsecond
                Backslash: \\
                Single quote: \'
                Double quote: \"
                Unicode: \u2603 (snowman)
                Trailing space preserved:\s
                This line continues \
                on the same line.
                """;
        System.out.println(allEscapes);

        // Null character and other rarely used escapes also work
        String special = """
                Backspace: \b
                Form feed: \f
                Carriage return: \r
                Octal: \101 (letter A)
                """;
        System.out.println(special);
    }
}

5. String Methods for Text Blocks

Java introduced three new instance methods on String that are designed to work with text blocks, though they can be used with any string. These methods give you programmatic control over the same operations the compiler performs automatically on text blocks.

Method Introduced Purpose
stripIndent() Java 12 Applies the text-block indentation algorithm to any string
translateEscapes() Java 12 Processes Java escape sequences in a string literal
formatted(Object... args) Java 15 Instance-method version of String.format()

stripIndent()

This method applies the same re-indentation algorithm that the compiler applies to text blocks. It is useful when you load multi-line text from a file or database and want to normalize the indentation.

public class StripIndentDemo {
    public static void main(String[] args) {

        // Simulating text loaded from external source with inconsistent indentation
        String fromFile = "        Line one\n" +
                          "        Line two\n" +
                          "            Line three (indented)\n" +
                          "        Line four\n";

        System.out.println("Before stripIndent():");
        System.out.println(fromFile);
        //         Line one
        //         Line two
        //             Line three (indented)
        //         Line four

        String stripped = fromFile.stripIndent();
        System.out.println("After stripIndent():");
        System.out.println(stripped);
        // Line one
        // Line two
        //     Line three (indented)
        // Line four

        // stripIndent() on a text block has no additional effect
        // because the compiler already stripped incidental whitespace
        String textBlock = """
                Hello
                World
                """;
        String doubleStripped = textBlock.stripIndent();
        System.out.println(textBlock.equals(doubleStripped)); // true
    }
}

translateEscapes()

This method processes Java escape sequences in a string. It is especially useful when reading strings from configuration files or user input where escape sequences appear as literal characters (e.g., the two characters \ and n) and you want to convert them to actual escape characters (e.g., a newline).

public class TranslateEscapesDemo {
    public static void main(String[] args) {

        // Simulating a string read from a config file
        // The file literally contains: Hello\nWorld (6 characters, not a newline)
        String raw = "Hello\\nWorld\\tTab\\\\Backslash";
        System.out.println("Raw: " + raw);
        // Raw: Hello\nWorld\tTab\\Backslash

        String translated = raw.translateEscapes();
        System.out.println("Translated:");
        System.out.println(translated);
        // Hello
        // World	Tab\Backslash

        // Works with all Java escape sequences
        String escapes = "\\t\\n\\\\\\'\\\"\\s".translateEscapes();
        System.out.println("Escapes: [" + escapes + "]");
        // Escapes: [	(tab, newline, backslash, quote, double-quote, space)]
    }
}

formatted()

The formatted() method is an instance-method equivalent of String.format(). Instead of writing String.format(template, args), you can write template.formatted(args). This is particularly clean with text blocks because it lets you chain the format call directly onto the text block.

public class FormattedDemo {
    public static void main(String[] args) {

        String name = "Alice";
        int age = 28;
        String city = "Seattle";

        // Old way: String.format()
        String old = String.format("Name: %s, Age: %d, City: %s", name, age, city);

        // New way: formatted() -- reads more naturally
        String modern = "Name: %s, Age: %d, City: %s".formatted(name, age, city);

        System.out.println(old.equals(modern)); // true

        // With text blocks -- this is where formatted() really shines
        String profile = """
                ================================
                User Profile
                ================================
                Name:     %s
                Age:      %d
                City:     %s
                Status:   %s
                ================================
                """.formatted(name, age, city, "Active");
        System.out.println(profile);

        // Building an HTML email template
        String emailHtml = """
                
                
                    

Welcome, %s!

Your account has been created successfully.

Account details:

  • Username: %s
  • Email: %s
  • Plan: %s

Get started at our dashboard.

""".formatted(name, name.toLowerCase(), "alice@example.com", "Premium", "https://example.com/dashboard"); System.out.println(emailHtml); } }

6. Practical Use Cases

Text blocks transform how you write embedded content in Java. Here are the real-world scenarios where they make the biggest difference.

JSON Strings

Building JSON in Java is one of the most common use cases. With text blocks, the JSON in your code looks exactly like the JSON it produces.

public class JsonTextBlocks {
    public static void main(String[] args) {

        // Static JSON
        String config = """
                {
                    "database": {
                        "host": "localhost",
                        "port": 5432,
                        "name": "myapp_db",
                        "pool": {
                            "min": 5,
                            "max": 20,
                            "timeout": 30000
                        }
                    },
                    "cache": {
                        "enabled": true,
                        "ttl": 3600,
                        "provider": "redis"
                    }
                }
                """;
        System.out.println(config);

        // Dynamic JSON with formatted()
        String userId = "usr_12345";
        String role = "admin";
        double balance = 1500.75;

        String userJson = """
                {
                    "id": "%s",
                    "role": "%s",
                    "balance": %.2f,
                    "permissions": ["read", "write", "delete"],
                    "metadata": {
                        "created": "2024-01-15T10:30:00Z",
                        "lastLogin": "2024-06-20T14:22:00Z"
                    }
                }
                """.formatted(userId, role, balance);
        System.out.println(userJson);
    }
}

SQL Queries

Complex SQL queries are dramatically more readable as text blocks. The query structure is visible at a glance.

public class SqlTextBlocks {
    public static void main(String[] args) {

        // Complex JOIN query
        String query = """
                SELECT
                    u.id,
                    u.username,
                    u.email,
                    p.display_name,
                    COUNT(o.id) AS order_count,
                    SUM(o.total) AS total_spent
                FROM users u
                INNER JOIN profiles p ON u.id = p.user_id
                LEFT JOIN orders o ON u.id = o.user_id
                WHERE u.status = 'ACTIVE'
                    AND u.created_at >= '2024-01-01'
                    AND u.email NOT LIKE '%@test.com'
                GROUP BY u.id, u.username, u.email, p.display_name
                HAVING COUNT(o.id) > 0
                ORDER BY total_spent DESC
                LIMIT 100
                """;
        System.out.println(query);

        // Dynamic query with formatted()
        String tableName = "products";
        String category = "electronics";
        int minPrice = 100;

        String dynamicQuery = """
                SELECT id, name, price, stock
                FROM %s
                WHERE category = '%s'
                    AND price >= %d
                    AND active = true
                ORDER BY price ASC
                """.formatted(tableName, category, minPrice);
        System.out.println(dynamicQuery);

        // CREATE TABLE statement
        String ddl = """
                CREATE TABLE IF NOT EXISTS employees (
                    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
                    first_name  VARCHAR(100) NOT NULL,
                    last_name   VARCHAR(100) NOT NULL,
                    email       VARCHAR(255) NOT NULL UNIQUE,
                    department  VARCHAR(100),
                    salary      DECIMAL(10,2),
                    hire_date   DATE NOT NULL,
                    status      ENUM('ACTIVE', 'INACTIVE', 'TERMINATED') DEFAULT 'ACTIVE',
                    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    updated_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

                    INDEX idx_email (email),
                    INDEX idx_department (department),
                    INDEX idx_status (status)
                );
                """;
        System.out.println(ddl);
    }
}

HTML Templates

public class HtmlTextBlocks {
    public static void main(String[] args) {

        String title = "Product Catalog";
        String productName = "Wireless Headphones";
        double price = 79.99;

        String html = """
                
                
                
                    
                    
                    %s
                    
                
                
                    

%s

%s

$%.2f

Premium wireless headphones with noise cancellation.

""".formatted(title, title, productName, price); System.out.println(html); } }

XML Configuration

public class XmlTextBlocks {
    public static void main(String[] args) {

        String appName = "MyApp";
        String version = "2.1.0";

        String pom = """
                
                
                    4.0.0

                    com.example
                    %s
                    %s
                    jar

                    
                        
                            org.springframework.boot
                            spring-boot-starter-web
                            3.2.0
                        
                        
                            org.postgresql
                            postgresql
                            42.7.1
                            runtime
                        
                    
                
                """.formatted(appName, version);
        System.out.println(pom);
    }
}

Error Messages and Logging

public class ErrorMessageTextBlocks {
    public static void main(String[] args) {

        String endpoint = "/api/users/123";
        int statusCode = 404;
        String method = "GET";

        String errorMessage = """
                ╔══════════════════════════════════════════╗
                ║          API ERROR REPORT                ║
                ╠══════════════════════════════════════════╣
                ║ Endpoint:    %s
                ║ Method:      %s
                ║ Status:      %d
                ║ Timestamp:   2024-06-20T14:30:00Z
                ╠══════════════════════════════════════════╣
                ║ The requested resource was not found.    ║
                ║ Please verify the resource ID and retry. ║
                ╚══════════════════════════════════════════╝
                """.formatted(endpoint, method, statusCode);
        System.out.println(errorMessage);
    }
}

Regex Patterns

Regular expressions benefit enormously from text blocks because you avoid double-escaping. In a regular string, matching a literal backslash requires "\\\\". In a text block, it is just \\.

import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class RegexTextBlocks {
    public static void main(String[] args) {

        // Old way: escape nightmare
        String oldPattern = "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{2}:\\d{2}";

        // Text block way: still need regex escapes, but no double-escaping for Java
        // Actually, for single-line regex, regular strings work fine.
        // Text blocks shine for COMPLEX, COMMENTED regex using (?x) verbose mode:

        String emailRegex = """
                (?x)                    # Enable verbose mode
                ^                       # Start of string
                [a-zA-Z0-9._%+-]+      # Local part: alphanumeric and special chars
                @                       # The @ symbol
                [a-zA-Z0-9.-]+         # Domain name
                \\.                     # Literal dot
                [a-zA-Z]{2,}           # Top-level domain (2+ chars)
                $                       # End of string
                """;

        Pattern pattern = Pattern.compile(emailRegex);

        String[] testEmails = {
            "user@example.com",
            "john.doe+work@company.co.uk",
            "invalid@",
            "@nodomain.com",
            "valid123@test.org"
        };

        for (String email : testEmails) {
            boolean matches = pattern.matcher(email).matches();
            System.out.printf("%-30s -> %s%n", email, matches ? "VALID" : "INVALID");
        }
    }
}

7. Text Blocks vs String Concatenation

Let us put text blocks side by side with the old approach. These before/after comparisons make the readability improvement undeniable.

public class BeforeAfterComparison {
    public static void main(String[] args) {

        // ===== EXAMPLE 1: JSON =====

        // BEFORE: String concatenation
        String jsonOld = "{\n" +
                "  \"user\": {\n" +
                "    \"name\": \"John\",\n" +
                "    \"roles\": [\"admin\", \"user\"],\n" +
                "    \"settings\": {\n" +
                "      \"theme\": \"dark\",\n" +
                "      \"notifications\": true\n" +
                "    }\n" +
                "  }\n" +
                "}";

        // AFTER: Text block
        String jsonNew = """
                {
                  "user": {
                    "name": "John",
                    "roles": ["admin", "user"],
                    "settings": {
                      "theme": "dark",
                      "notifications": true
                    }
                  }
                }
                """;


        // ===== EXAMPLE 2: SQL =====

        // BEFORE
        String sqlOld = "SELECT e.id, e.name, d.department_name, " +
                "COUNT(p.id) AS project_count " +
                "FROM employees e " +
                "JOIN departments d ON e.dept_id = d.id " +
                "LEFT JOIN projects p ON e.id = p.lead_id " +
                "WHERE e.status = 'ACTIVE' " +
                "GROUP BY e.id, e.name, d.department_name " +
                "ORDER BY project_count DESC";

        // AFTER
        String sqlNew = """
                SELECT e.id, e.name, d.department_name,
                       COUNT(p.id) AS project_count
                FROM employees e
                JOIN departments d ON e.dept_id = d.id
                LEFT JOIN projects p ON e.id = p.lead_id
                WHERE e.status = 'ACTIVE'
                GROUP BY e.id, e.name, d.department_name
                ORDER BY project_count DESC
                """;


        // ===== EXAMPLE 3: Multi-line error message =====

        // BEFORE
        String errorOld = "Error: Invalid configuration detected.\n" +
                "  - Property 'database.url' is required but missing.\n" +
                "  - Property 'server.port' must be between 1024 and 65535.\n" +
                "  - Property 'cache.ttl' must be a positive integer.\n" +
                "\n" +
                "Please check your application.properties file and fix the above issues.";

        // AFTER
        String errorNew = """
                Error: Invalid configuration detected.
                  - Property 'database.url' is required but missing.
                  - Property 'server.port' must be between 1024 and 65535.
                  - Property 'cache.ttl' must be a positive integer.

                Please check your application.properties file and fix the above issues.""";


        // They produce the same strings
        System.out.println(jsonOld.equals(jsonNew.stripTrailing()));
        System.out.println(errorOld.equals(errorNew));
    }
}

Readability comparison summary:

Aspect String Concatenation Text Block
Escape characters Required for " and \n Not needed for quotes or newlines
Newlines Explicit \n on every line Automatic from source line breaks
Concatenation operators + on every line None needed
Visual match to output Obscured by syntax noise Content looks like the output
Maintenance effort High — easy to break escaping Low — edit like a text file
Copy-paste from external tool Requires reformatting Paste directly, usually works

8. Text Blocks with String.format and formatted()

In real applications, strings rarely contain only static content. You need to inject dynamic values — user names, timestamps, computed quantities. Text blocks work seamlessly with both String.format() and the newer formatted() method.

public class DynamicTextBlocks {
    public static void main(String[] args) {

        // === Method 1: String.format() ===
        String template1 = """
                Dear %s,

                Your order #%d has been confirmed.
                Total: $%.2f
                Estimated delivery: %s

                Thank you for shopping with us!
                """;
        String email1 = String.format(template1, "Alice", 98765, 249.99, "March 15, 2024");
        System.out.println(email1);

        // === Method 2: formatted() -- cleaner, more fluent ===
        String email2 = """
                Dear %s,

                Your subscription to the %s plan is now active.
                Monthly charge: $%.2f
                Next billing date: %s

                Manage your subscription at: %s
                """.formatted("Bob", "Premium", 29.99, "April 1, 2024", "https://example.com/account");
        System.out.println(email2);

        // === Method 3: String.replace() for named placeholders ===
        String template3 = """
                {
                    "event": "{{EVENT_TYPE}}",
                    "user": "{{USER_ID}}",
                    "timestamp": "{{TIMESTAMP}}",
                    "payload": {
                        "action": "{{ACTION}}",
                        "resource": "{{RESOURCE}}"
                    }
                }
                """;
        String event = template3
                .replace("{{EVENT_TYPE}}", "user.action")
                .replace("{{USER_ID}}", "usr_abc123")
                .replace("{{TIMESTAMP}}", "2024-06-20T10:30:00Z")
                .replace("{{ACTION}}", "delete")
                .replace("{{RESOURCE}}", "document_456");
        System.out.println(event);

        // === Method 4: MessageFormat for positional arguments ===
        // (useful when you need to reuse the same argument)
        // import java.text.MessageFormat;
        String template4 = """
                Hello {0}! Welcome back, {0}.
                You have {1} unread messages.
                Last login: {2}
                """;
        String greeting = java.text.MessageFormat.format(
                template4, "Charlie", 5, "yesterday");
        System.out.println(greeting);
    }
}

Building a Template Engine with Text Blocks

Here is a practical example of building a simple template engine using text blocks and replace():

import java.util.Map;

public class SimpleTemplateEngine {

    private final String template;

    public SimpleTemplateEngine(String template) {
        this.template = template;
    }

    public String render(Map variables) {
        String result = template;
        for (Map.Entry entry : variables.entrySet()) {
            result = result.replace("${" + entry.getKey() + "}", entry.getValue());
        }
        return result;
    }

    public static void main(String[] args) {

        // Define the template using a text block
        SimpleTemplateEngine engine = new SimpleTemplateEngine("""
                
                
                ${title}
                
                    

Welcome, ${username}!

Account type: ${accountType}

Member since: ${memberSince}

Credits remaining: ${credits}

${footerText}

"""); // Render with variables String html = engine.render(Map.of( "title", "My Dashboard", "username", "Alice", "accountType", "Premium", "memberSince", "January 2023", "credits", "1,250", "footerText", "Copyright 2024 Example Corp." )); System.out.println(html); } }

9. Common Pitfalls

Text blocks are intuitive once you understand the rules, but there are several traps that catch developers — especially during the first few weeks of using them.

Pitfall 1: Trailing Whitespace Gets Stripped

This is the most common surprise. Spaces and tabs at the end of each line in a text block are silently removed during compilation. If your string depends on trailing spaces (e.g., fixed-width formatting, test assertions), the output will not match your expectations.

public class PitfallTrailingWhitespace {
    public static void main(String[] args) {

        // You might expect 10-char wide columns, but trailing spaces are stripped
        String table = """
                Name      Age       City
                Alice     30        Seattle
                Bob       25        Portland
                """;

        // Actual content: "Name      Age       City\nAlice     30        Seattle\n..."
        // The trailing spaces after "City", "Seattle", "Portland" are GONE

        // FIX: Use \s at the end of each line
        String fixedTable = """
                Name      Age       City      \s
                Alice     30        Seattle   \s
                Bob       25        Portland  \s
                """;
        // Now trailing spaces are preserved because \s prevents stripping
    }
}

Pitfall 2: Missing Trailing Newline

Whether your string ends with a newline depends on where you place the closing """. This matters in tests and when comparing strings.

public class PitfallMissingNewline {
    public static void main(String[] args) {

        // CAREFUL: These two produce DIFFERENT strings
        String withNewline = """
                Hello""";       // No trailing newline -- value is "Hello"

        String alsoWithNewline = """
                Hello
                """;            // Has trailing newline -- value is "Hello\n"

        System.out.println(withNewline.equals(alsoWithNewline));     // false!
        System.out.println(withNewline.length());                     // 5
        System.out.println(alsoWithNewline.length());                 // 6

        // This can break assertEquals in tests:
        // assertEquals("Hello\n", textBlock); // might fail if closing """ is inline
    }
}

Pitfall 3: Unexpected Indentation

If you do not understand the common-whitespace-removal algorithm, you might get more or fewer leading spaces than you expect.

public class PitfallIndentation {
    public static void main(String[] args) {

        // SURPRISE: This has NO indentation in the output
        String noIndent = """
                {
                    "key": "value"
                }
                """;
        // Common whitespace = 16 spaces (all lines have at least 16)
        // Result: "{\n    \"key\": \"value\"\n}\n"
        // The 16 spaces are stripped; the 4 extra spaces on "key" remain

        // SURPRISE: This has 4 spaces of indentation on ALL lines
        String withIndent = """
                {
                    "key": "value"
                }
            """;
        // Common whitespace = 12 (from closing """)
        // All content lines have 16 spaces, so 16-12 = 4 remain
        // Result: "    {\n        \"key\": \"value\"\n    }\n"

        // TIP: To control indentation precisely, focus on the closing """
        System.out.println("--- No indent ---");
        System.out.println(noIndent);
        System.out.println("--- With indent ---");
        System.out.println(withIndent);
    }
}

Pitfall 4: Three Consecutive Quotes in Content

If your content contains three or more consecutive double-quote characters, you must escape at least one of them. Otherwise, the compiler interprets them as the closing delimiter.

public class PitfallConsecutiveQuotes {
    public static void main(String[] args) {

        // COMPILE ERROR: the compiler sees """ as the closing delimiter
        // String bad = """
        //         She said """Hello""" loudly
        //         """;

        // FIX 1: Escape the first quote of the triple
        String fix1 = """
                She said \"""Hello\""" loudly
                """;

        // FIX 2: Escape the last quote before the triple
        String fix2 = """
                She said ""\"""Hello""\""" loudly
                """;

        // In practice, three consecutive quotes are rare in JSON/SQL/HTML,
        // so this pitfall usually only matters for documentation or test strings.
    }
}

Pitfall 5: Mixing Tabs and Spaces

The common-whitespace algorithm treats tabs and spaces as different characters. If some lines use tabs for indentation and others use spaces, the common prefix will be shorter than you expect, resulting in extra whitespace in the output. Always use consistent indentation — ideally spaces only — within text blocks.

public class PitfallMixedIndentation {
    public static void main(String[] args) {

        // If your IDE mixes tabs and spaces, the result may surprise you.
        // Stick to one indentation style within text blocks.

        // All spaces -- predictable
        String clean = """
                Line one
                Line two
                Line three
                """;

        // Tip: Configure your IDE to insert spaces instead of tabs
        // IntelliJ: Settings > Editor > Code Style > Java > Use tab character: OFF
        // VS Code: "editor.insertSpaces": true
    }
}

10. Best Practices

After working with text blocks across production codebases, here are the guidelines that lead to clean, maintainable code.

When to Use Text Blocks

Use Text Blocks When Use Regular Strings When
String spans 2+ lines String fits on one line
Content has double quotes (JSON, HTML, XML) No special characters
Readability is improved by visual structure String is short and simple
You are embedding another language (SQL, JSON, YAML) Dynamic string built from variables
Test fixtures and expected output Simple error messages or labels

Formatting Conventions

public class BestPractices {

    // GOOD: Closing """ aligned with content for no extra indentation
    private static final String CREATE_USER_SQL = """
            INSERT INTO users (name, email, role)
            VALUES (?, ?, ?)
            """;

    // GOOD: Using formatted() for dynamic content
    public static String buildGreeting(String name, int unreadCount) {
        return """
                Hello, %s!
                You have %d unread messages.
                """.formatted(name, unreadCount);
    }

    // GOOD: Text block constants as static final fields
    private static final String ERROR_TEMPLATE = """
            Error in module: %s
            Message: %s
            Suggestion: %s
            """;

    // GOOD: Using replace() for named templates
    private static final String NOTIFICATION_TEMPLATE = """
            Hi ${name},
            Your ${itemType} "${itemName}" has been ${action}.
            """;

    public static String buildNotification(String name, String itemType,
                                            String itemName, String action) {
        return NOTIFICATION_TEMPLATE
                .replace("${name}", name)
                .replace("${itemType}", itemType)
                .replace("${itemName}", itemName)
                .replace("${action}", action);
    }

    // AVOID: Don't use text blocks for single-line strings
    // Bad:
    String overkill = """
            Hello, World!
            """;
    // Good:
    String simple = "Hello, World!";

    // AVOID: Don't use text blocks when StringBuilder is more appropriate
    // If you're building a string in a loop, StringBuilder is still the right tool.

    public static void main(String[] args) {

        System.out.println(buildGreeting("Alice", 5));

        System.out.println(buildNotification(
                "Bob", "document", "Q4 Report", "approved"));

        // Text blocks are String objects -- all String methods work
        String json = """
                {
                    "key": "value"
                }
                """;
        System.out.println("Length: " + json.length());
        System.out.println("Contains key: " + json.contains("key"));
        System.out.println("Uppercase:\n" + json.toUpperCase());
        System.out.println("Lines: " + json.lines().count());
    }
}

Summary of Key Rules

  • Opening """ must be followed by a newline — no content on the same line
  • Closing """ position controls indentation — align it with content for zero indentation
  • Trailing whitespace is stripped — use \s to preserve it
  • Line continuation \ joins lines — great for long single-line strings
  • formatted() is your friend — chain it directly onto text blocks
  • Text blocks produce regular Strings — all existing methods and patterns work
  • Use consistent indentation — spaces only, no tabs mixed with spaces
  • Declare as static final when the text block is a constant template

Text blocks are one of those features that, once you start using, you wonder how you ever lived without. They eliminate an entire category of string-formatting bugs and make your code look like it respects the developer who has to read it next. Start using them for any multi-line string in your codebase — your future self will thank you.




Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

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