Text Blocks: Multi-Line String Literals
Objective
Understand the text block (""", JEP 378, finalized in Java 15): a multi-line string literal that removes the \n-at-every-line-break, +-at-every-line-end, escape-every-quote noise of building multi-line text with ordinary "..." literals. A text block is source syntax only — the compiler produces exactly a String, with the same type, the same methods, and the same interning as an equivalent regular literal.
Use Cases
- Embedding a multi-line SQL query, JSON payload, or HTML/XML fragment directly in Java source, without a wall of concatenation and escapes.
- Writing a test's expected multi-line output (snapshot-style assertions) in a form that visually matches the thing it represents.
- Any literal that previously needed a
String.join("\n", ...)call, aStringBuilder, or a helper method just to stay readable.
Deep Dive
The opening delimiter must end its line
A text block starts with three double quotes followed by nothing but optional trailing spaces and a line terminator. Content begins on the next line:
javaString bad = """Hello, world"""; // error: illegal text block open delimiter sequence,
// missing line terminatorjavaString good = """
Hello, world
""";The closing delimiter may sit on its own line, or at the end of the last content line. That choice decides whether the value ends with a newline:
javaString withNewline = """
one
two
"""; // "one\ntwo\n"
String noNewline = """
one
two"""; // "one\ntwo" — no trailing \nIncidental whitespace: the closing delimiter is part of the input
This is the mechanic that surprises everyone. The compiler looks at every non-blank content line and the line containing the closing delimiter, takes the minimum leading-whitespace count across them, and strips exactly that many leading white space characters from every line.
javaclass Report {
static String sql() {
return """
SELECT id, name
FROM users
WHERE active = true
""";
}
}Content lines are indented 16, 18, and 17 columns; the closing """ sits at column 16. The minimum is 16, so 16 columns come off every line:
textSELECT id, name FROM users WHERE active = true
Now move only the closing delimiter four columns left — the content is untouched:
java return """
SELECT id, name
FROM users
WHERE active = true
""";The minimum is now 12, so only 12 columns are stripped and every line keeps four leading spaces:
textSELECT id, name FROM users WHERE active = true
The delimiter's column is genuinely an input to the algorithm, not just a terminator. Note the asymmetry: pushing the closing """ further right than the least-indented content line changes nothing (the content minimum still wins), while pushing it left adds indentation to the value. Blank lines do not participate in the minimum — they are simply normalized to empty lines.
Trailing whitespace is always stripped
Independent of the indentation algorithm, trailing white space is removed from the end of every line:
javaString s = """
alpha
beta
"""; // "alpha\nbeta\n" — the three spaces after "alpha" are goneThis is deliberate (it makes invisible trailing spaces impossible to introduce by accident in a diff), but it does mean a text block is not a byte-for-byte copy of what you typed. To keep a trailing space, you need \s (below).
Line terminators are normalized to \n
Whatever the source file uses — LF, CRLF, or CR — every line terminator inside a text block becomes a single \n in the resulting String. A file authored on Windows and one authored on Linux compile to the identical value:
javaString twoLines = """
first
second
""";
// twoLines.equals("first\nsecond\n") is true on every platform
assert !twoLines.contains("\r");If you genuinely need CRLF in the value, write it explicitly with \r\n escapes.
Quotes and escapes inside a text block
All the familiar escapes still work (\n, \t, \\, \", unicode escapes). The difference is that a lone " needs no escaping at all, because only a run of three quotes is ambiguous:
javaString quoted = """
She said "yes" — a single quote pair needs no escape.
Two in a row are fine as well: ""
Three would close the block, so escape one of them: \"""
""";Any of \""", "\"", or ""\" works; only three-or-more consecutive quotes require an escape.
Escape processing happens after incidental whitespace stripping, which is why a \n written inside a text block never confuses the indentation algorithm — at stripping time it is still two characters, not a line break.
The two escapes unique to text blocks: \s and line continuation
\s translates to a single space (U+0020). Because it is translated after trailing-whitespace stripping, it acts as a fence that protects everything to its left:
javaString padded = """
red \s
green\s
blue
""";
// "red \ngreen \nblue\n"
// ^^^ two typed spaces + the \s space surviveA backslash at the very end of a line suppresses that line's terminator, letting one logical line be wrapped across several physical source lines:
javaString oneLine = """
The quick brown fox \
jumps over \
the lazy dog.
""";
// "The quick brown fox jumps over the lazy dog.\n"The space before each \ is preserved: the line no longer *ends* in whitespace, so there is nothing for the trailing-whitespace pass to strip. This line-continuation escape is not valid in an ordinary "..." literal — it exists only for text blocks.
Still exactly a String
A text block is a string literal, so it is a compile-time constant expression whenever its content is fully known at compile time — usable as a static final constant, an annotation value, or a switch case label, and interned like any other literal:
javastatic final String GREETING = """
hello""";
GREETING == "hello"; // true — constant expression, same interned instanceThere is no interpolation, so variables go in through the normal String API:
javaString body = """
{"user": "%s", "id": %d}
""".formatted(name, id);Java 15 also exposed the algorithm at runtime: String.stripIndent() applies the same incidental-whitespace rules to an ordinary string, and String.translateEscapes() performs escape translation — handy when the text arrives from a file rather than from source.
Trade-offs
- The closing delimiter's column silently changes the value. An auto-formatter, or a careless re-indent during review, can shift the closing
"""and change the runtime string with no compiler warning — both versions are perfectly valid syntax. Golden-file tests andassertEqualson multi-line output are where this bites:
javaString a = """
x
"""; // "x\n"
String b = """
x
"""; // " x\n" — same content, delimiter 4 columns left- No interpolation. Unlike template literals in other languages,
${...}and friends do not exist here; injecting a value still meansformatted(),String.format(), or concatenation — and for SQL specifically, that means a text block does nothing to help you avoid injection, so parameter placeholders remain mandatory:
javaString sql = """
SELECT * FROM users WHERE id = ?
"""; // still a PreparedStatement parameter, not string-builtTrailing whitespace cannot be typed, only escaped. Any content whose meaning depends on trailing spaces (fixed-width record formats, some Markdown line breaks) needs an explicit
\son every affected line, which is easy to forget and invisible in the value until a test fails.Zero runtime distinction. A
Stringcarries no marker of having come from a text block, so nothing can detect "was this literal a text block" reflectively or at runtime — it is purely a source-ergonomics feature, and an API must never be designed around telling the two apart:
java"a\nb\n".equals("""
a
b
"""); // true — indistinguishable