JAVA / STRINGS AND TEXT HANDLING
Text blocks for multiline and embedded quotes
Write multiline literals with text blocks, keep embedded quotes unescaped, and control indentation and the trailing newline with the closing delimiter.
What you will learn
- Open with """ plus a line break, since nothing may follow the opening delimiter
- Embed double quotes, JSON and HTML attributes without a single backslash escape
- Predict stripping: the least indented line and the closing """ set the left edge
- Use \ to drop a line break and \s to keep a trailing space that stripping removes
Understanding Text blocks for multiline and embedded quotes
A text block is not a new kind of string. Three double quotes followed by a line terminator open a literal whose value is an ordinary String, interned like any other literal, so a text block is legal anywhere a constant is: annotation values, case labels, static final fields. What changes is only how the compiler reads the characters between the delimiters: a real line break in the source becomes a \n in the value, and a lone " is just a character, because only three in a row can end the literal.
Between the delimiters the compiler runs a fixed three-step pass. It first normalizes every line terminator to \n so the value does not depend on how the file was saved; then it removes incidental white space, meaning it finds the smallest indentation among all non-blank lines plus the line holding the closing """, deletes that many leading white space characters from every line, and strips trailing white space from each line; only then does it interpret escapes. The mental model is a rectangle of text where you are choosing a vertical left edge, and the closing delimiter is one of the lines that gets a vote on where that edge falls. Push the closing """ further left and more of the source indentation survives into the value.
Because escapes are interpreted last, they act on text that the indentation pass has already finished with, and that ordering is what makes two of them useful. \s becomes a single space, but at stripping time it is still a backslash and an s, so it shields the spaces to its left from trailing-white-space removal; a \ as the last character of a line cancels that line break, which lets you spread one long line of text over several source lines, or keep the closing delimiter on its own line without gaining a final \n. There is no interpolation of any kind: a $ or a {name} inside a block is plain text, and substitution is done afterwards with formatted.
public class TextBlockTour {
public static void main(String[] args) {
String html = """
<p class="note">
He said "no escaping needed" here.
</p>""";
String withNewline = """
done
""";
System.out.println(html);
System.out.println("[" + withNewline + "]");
System.out.println("html lines: " + html.lines().count());
System.out.println("ends with newline: " + html.endsWith("\n"));
}
}A text block is a plain String literal whose value the compiler derives by normalizing line endings and stripping the indentation shared by every line and the closing delimiter, before any escape is interpreted.
Worked examples
The closing delimiter votes on indentation
Two blocks with identical content lines produce different indentation because their closing delimiters sit in different columns.
public class Indentation {
public static void main(String[] args) {
String left = """
a
b
""";
String shifted = """
a
b
""";
System.out.println("left:\n[" + left.replace(" ", ".") + "]");
System.out.println("shifted:\n[" + shifted.replace(" ", ".") + "]");
}
}Example explained
Line 1Both blocks hold the same two content lines: a at column 12 and b at column 14.
Line 2In left the closing """ is also at column 12, so 12 characters are incidental and a loses all its indentation while b keeps two spaces.
Line 3In shifted the closing """ moved to column 8, lowering the minimum to 8, so four spaces survive on a and six on b.
Line 4replace(" ", ".") makes the surviving spaces visible, and both values end in one \n because each closing delimiter is alone on its line.
Cancelling newlines and keeping spaces
Shows \ as a line-join escape and \s as a space that also blocks trailing-white-space stripping.
public class Escapes {
public static void main(String[] args) {
String oneLine = """
Roses are red, \
violets are blue.\
""";
String padded = """
id \s
name\s
""";
System.out.println("[" + oneLine + "]");
System.out.print(padded.replace(" ", "_"));
}
}Example explained
Line 1A \ immediately before a line break cancels that break, so the two source lines of oneLine become a single line of text.
Line 2The second \ swallows the newline before the closing delimiter, which is how you keep the delimiter on its own line and still avoid a trailing \n.
Line 3Trailing white space is removed from every line, but when that happens \s is still a backslash and an s, so the two spaces after id are not trailing and survive.
Line 4Escapes run last and turn each \s into one space, giving id three spaces and name one.
Three quotes in a row, and filling in values
Escaping one quote lets a block contain the delimiter itself, and formatted supplies the substitution that text blocks do not do.
public class Quoting {
public static void main(String[] args) {
String doc = """
Use \""" to open a text block.
""";
String greet = """
{"user": "%s", "age": %d}
""".formatted("Ada", 36);
System.out.print(doc);
System.out.print(greet);
}
}Example explained
Line 1Escaping only the first quote in \""" stops the lexer from seeing a closing delimiter, and the value still holds three real quote characters.
Line 2The quotes around %s and the JSON keys need no escape at all, since single and double quotes are unremarkable inside a block.
Line 3formatted is chained onto the closing delimiter because the block is an ordinary String expression, not special syntax.
Line 4Both values already end in \n from their own-line closing delimiters, so print is enough and println would add blank lines.
Important notes
Line terminators are normalized before anything else, so a text block never contains \r no matter whether the file uses CRLF or LF; a test that compares against "line\r\n" will not match.
Indentation is counted in white space characters, not visual columns: one tab counts as one character, so mixing tabs and spaces in a block makes the stripped result hard to predict.
Common mistakes
Writing String s = """hello"""; on one line. The compiler rejects it with "illegal text block open delimiter sequence, missing line terminator", because content may never start on the opening line.
Aligning the closing """ with the String keyword rather than with the content, which leaves every line of the value indented and makes equals comparisons against the expected text fail for reasons that are invisible on screen.
Forgetting that a closing """ on its own line contributes a final \n, so a block used as a SQL statement, HTTP header or map key silently carries a newline that the server or lookup does not expect.
Try it yourself
Change, predict, then run
Take String s = "<ul>\n <li>a=\"1\"</li>\n</ul>"; and write a text block that makes s.equals(block) print true, then move the closing """ down onto its own line and confirm the comparison flips to false.
Open the Java workspaceCheck your understanding
A text block has its first content line one indented six spaces, its second content line two indented eight spaces, and its closing """ alone on a line indented two spaces. What String does it produce?
- " one\n two\n"
- "one\n two\n"
- " one\n two"
- " one\n two\n"
Show answer
The line holding the closing delimiter takes part in the minimum-indentation vote along with the non-blank content lines, and at two spaces it is the smallest, so exactly two characters are stripped from each line, leaving four spaces before one and six before two. "one\n two\n" is tempting because it is what you would get if only the content lines were measured, which is the same as moving the closing """ up under one; and the value does end in \n precisely because that delimiter sits on its own line.