Java 17 Text Blocks

July 25, 20264 min readUpdated 8/20/2026

A text block is a string literal delimited by three quotes that can span lines without escaping. It exists because embedded JSON, SQL and HTML were genuinely painful to read in Java, and the workarounds — concatenation, \n everywhere, external files — were all worse than the problem.

The problem

class Demo {
    void run() {
        // Every quote escaped, every newline explicit, and the shape of the JSON is invisible
        String json = "{\n"
                + "  \"name\": \"Folau\",\n"
                + "  \"role\": \"engineer\"\n"
                + "}";

        String block = """
                {
                  "name": "Folau",
                  "role": "engineer"
                }
                """;

        System.out.println(json.length() > 0 && block.length() > 0);
    }
}

The second version is the same string. Quotes need no escaping, newlines are where you put them, and — the real gain — you can paste the JSON in and read it back.

The rules

Three things the compiler is strict about:

  • The opening """ must be followed by a newline. String s = """hello"""; does not compile. Content starts on the next line.
  • The content is everything up to the closing """, with incidental indentation removed — see below.
  • A trailing newline is included if the closing delimiter is on its own line, and omitted if it sits at the end of the last content line.
class Demo {
    void run() {
        String withNewline = """
                hello
                """;                     // "hello\n"

        String without = """
                hello""";                // "hello"

        System.out.println(withNewline.length());   // 6
        System.out.println(without.length());       // 5
    }
}

Indentation — the part worth understanding

Java strips the common leading whitespace, so the block can be indented to match the surrounding code without that indentation ending up in the string. The position of the closing """ participates in the calculation:

class Demo {
    void run() {
        String aligned = """
                line one
                line two
                """;                     // closing aligned with content -> no indent in the string

        String indented = """
                line one
                line two
        """;                             // closing further LEFT -> content keeps 8 spaces

        System.out.println(aligned.lines().findFirst().orElse(""));   // "line one"
        System.out.println(indented.lines().findFirst().orElse(""));  // "        line one"
    }
}

The rule: the compiler finds the least-indented non-blank line, including the closing delimiter line, and removes that much from every line. Moving the closing """ therefore changes the indentation of the whole string, which surprises people exactly once.

Trailing whitespace on each line is also stripped, which is usually invisible and occasionally matters. \s escapes a space you want kept.

Escapes that still work, and two new ones

class Demo {
    void run() {
        // Ordinary escapes still apply
        String tabbed = """
                name\tvalue
                """;

        // \ at end of line: join this line to the next, no newline
        String joined = """
                this is one \
                long line
                """;
        System.out.println(joined);        // this is one long line

        // \s: keep a trailing space that would otherwise be stripped
        String padded = """
                two spaces  \s
                """;
        System.out.println(padded.lines().findFirst().orElse("").length());   // 13
    }
}

The line-continuation backslash is the more useful of the two. It lets you lay out a long single-line string — a URL, a query — across several source lines without introducing newlines into the value.

Where they earn their place

class Demo {
    void run() {
        String sql = """
                SELECT c.id, c.name, COUNT(o.id) AS orders
                FROM   customers c
                LEFT JOIN orders o ON o.customer_id = c.id
                WHERE  c.active = true
                GROUP BY c.id, c.name
                ORDER BY orders DESC
                """;

        String html = """
                <ul>
                  <li>first</li>
                  <li>second</li>
                </ul>
                """;

        System.out.println(sql.lines().count() + html.lines().count());   // 10
    }
}

SQL is the strongest case. A query written this way can be copied straight into a database client and back, which is not true of a concatenated one — and reviewing a query you can actually read catches bugs that reviewing string fragments does not.

Formatting values in

A text block is a compile-time constant, so no interpolation happens — Java has none. Use formatted, which reads well at the end of a block:

class Demo {
    String message(String name, int orders) {
        return """
                Hello %s,

                You have %d orders waiting.
                """.formatted(name, orders);
    }
}

Watch for literal % characters in the block if you do this — they need doubling as %%, which is a real trap in a block containing CSS or a LIKE clause.

They are ordinary strings

Worth stating plainly, because the syntax makes them look like a distinct type: a text block produces a String, identical in every way to one written with single quotes. There is no runtime cost, no wrapper, and no new class.

class Demo {
    void run() {
        String a = "hello\nworld\n";
        String b = """
                hello
                world
                """;

        System.out.println(a.equals(b));    // true — the same string
        System.out.println(a == b);         // true — both interned constants
    }
}

Because the value is computed at compile time, a text block is a constant expression: it can be used in a switch label, as an annotation value, and it is interned in the string pool like any other literal. The == above returning true is the same pooling behaviour the String post describes — and just as unwise to rely on.

The corollary is that anything computed at runtime cannot be inside the block, which is why formatting is a separate step rather than interpolation.

When not to use one

A text block is still a string in your class file, so it is not a replacement for externalising content. Anything a non-developer should edit, anything localised, and anything large belongs in a resource file. Text blocks are for short embedded fragments where keeping the code next to what it does is worth more than the flexibility of a separate file.

Next

Pattern matching for instanceof is next — the end of casting immediately after a type check.