1. Overview
In this lesson, we’ll explore text blocks, a Java 15 feature that simplifies writing multi-line strings. We’ll look at the new “”” syntax, indentation rules, practical examples for embedding code, and new formatting controls and helper methods.
The relevant module we need to import when starting this lesson is: multi-line-strings-with-text-blocks-start.
If we want to reference the fully implemented lesson, we can import: multi-line-strings-with-text-blocks-end.
2. The Old Way: Problems with Multi-line Strings
Before Text Blocks, if we wanted to create a String that spanned multiple lines, we had to use a combination of string concatenation (+) and explicit newline escape characters (\n).
This was especially painful when embedding other languages, like JSON or SQL. To write a simple JSON object, we had to do this:
String oldJson = "{\n" +
" \"name\": \"Learn Java Task\",\n" +
" \"description\": \"Let's learn Java new features\"\n" +
"}";
This is hard to read and even harder to maintain. We have to add all the linebreak characters (\n) manually, and escape every double quote in the JSON structure. In this example, if we wanted to copy and paste this JSON into a validator, we’d have to remove all the Java-specific formatting manually.
3. The New Syntax: “””
Text blocks solve this problem completely. A text block is a multi-line string literal that begins with three double-quote characters (“””) followed by a line terminator. The block is closed by another set of three double-quote characters.
Let’s rewrite our ugly JSON string from before as a text block. We can add a new test to our NewJavaFeaturesUnitTest class to demonstrate:
@Test
void whenUsingTextBlock_thenJsonIsReadableAndCorrect() {
String textBlockJson = """
{
"name": "Learn Java Task",
"description": "Let's learn Java new features"
}
""";
// Assertions
Stream<String> lines = textBlockJson.lines();
Stream<String> expectedLines = Stream.of(
"{",
" \"name\": \"Learn Java Task\",",
" \"description\": \"Let's learn Java new features\"",
"}"
);
assertLinesMatch(expectedLines, lines);
}
As we can see, the JSON code in the text block is much easier to read. Further, we can copy and paste code snippets directly into other editors without any modification. Notice we don’t need to escape the quotes or manually add newlines.
It’s worth noting that in this example, we used String.lines() to convert the string, separated by line terminators, into a Stream<String>. Later, we leverage JUnit 5’s assertLinesMatch() method in the assertion to verify if textBlockJson contains expected lines. We’ll use this technique in most test methods in this lesson.
4. How Indentation Is Handled
The first question we might have is, “What about all the indentation from my method?” Text blocks are smart enough to handle this automatically. The compiler distinguishes between two categories of indentation: incidental whitespace and essential whitespace.
Incidental whitespace is the indentation we use to align the text block with our surrounding code, such as the spaces before { and } in our JSON example. The Java compiler automatically removes these space characters.
On the other hand, essential whitespace refers to the indentation within the block that we actually want to preserve, such as the spaces before “name” and “description”. This indentation is preserved.
4.1. Incidental Whitespace
So, how does the compiler know what is “incidental”? Simply put, it determines this by finding the line with the least amount of indentation.
The compiler examines the indentation of all non-blank lines. Also, it considers the indentation of the closing “”” line if it’s on its own line. It finds the minimum indentation among all these lines, which is considered “incidental” and is then removed from the beginning of every line in the block.
This algorithm is almost always intuitive. In practice, the indentation of the closing “”” often determines the baseline for the entire block because we typically place it at the same level as (or to the left of) the content.
Let’s add a test to prove this. We’ll create a block where the content and the closing “”” are aligned:
@Test
void whenTextBlockIsIndented_thenIncidentalWhitespaceIsRemoved() {
String indentedBlock = """
{
"name": "Learn Java Task"
}
""";
Stream<String> lines = indentedBlock.lines();
Stream<String> expectedLines = Stream.of(
"{",
" \"name\": \"Learn Java Task\"",
"}"
);
assertLinesMatch(expectedLines, lines);
}
As we can see, the test verifies that inside the text block, the indentation of the Java code is “incidental” and removed.
4.2. Essential Whitespace
Of course, knowing this, adding or removing essential whitespace is intuitive; we have to update the indentation within the text block accordingly.
However, let’s perform a quick experiment to see the incidental/essential relation more clearly: if we move the closing “”” to the left, it will add indentation to our string, because even though the general indentation of the block remains the same, the “incidental/essential” relationship is shifted:
@Test
void whenClosingQuotesAreShifted_thenIndentationIsAdded() {
// @formatter:off
String shiftedBlock = """
{
"name": "Learn Java Task"
}
""";
// @formatter:on
Stream<String> lines = shiftedBlock.lines();
Stream<String> expectedLines = Stream.of(
" {",
" \"name\": \"Learn Java Task\"",
" }"
);
assertLinesMatch(expectedLines, lines);
}
As we can see, we moved the closing “”” two spaces left. Note that we need to disable the auto-format to avoid the IDE moving the whole block back to the right to its “regular” position (which, of course, wouldn’t change the result, since it only increments the general indentation). The test shows that lines in our shiftedBlock string now start with two spaces of “essential” whitespace.
We should note that the closing “”” can also be placed on the same line as the last character of the text, like }”””;. In this case, that line is simply treated as the last line of content, its indentation is included in the “minimum” calculation, and the resulting string does not end with a newline character.
Additionally, the Java compiler performs line terminator normalization. Any carriage return (\r) or carriage-return/line-feed (\r\n) characters in the text block are automatically converted to a single line-feed (\n). This ensures the string’s line endings are consistent across all operating systems.
4.3. The indent() Method
We can also use the indent() method to set (i.e., increase) a custom indentation on each line:
@Test
void whenCallIndentMethod_thenIndentationIsCorrect() {
String indentedBlock = """
{
"name": "Learn Java Task"
}
""".indent(1);
Stream<String> lines = indentedBlock.lines();
Stream<String> expectedLines = Stream.of(
" {",
" \"name\": \"Learn Java Task\"",
" }"
);
assertLinesMatch(expectedLines, lines);
}
As shown in the example, we set a custom indent of 1 space on all lines in the text block.
5. Practical Examples
Text Blocks are perfect for embedding formatted code snippets. As we’ve already seen, JSON can be embedded directly.
Apart from JSON, text blocks make SQL queries in Java much easier, for example:
String sql = """
SELECT
c.name,
t.description
FROM
Campaign c
JOIN
Task t ON c.code = t.campaign_code
WHERE
c.code = ?
""";
Additionally, HTML is another great use case:
String html = """
<html>
<body>
<h1>Hello</h1>
<p>
In Java, "Hello" is a String. 'h' is a Character
</p>
</body>
</html>
""";
As we can see, we no longer need to escape every quote. Of course, we can use text blocks in many more cases. Text blocks are a concise, straightforward way to store structured text in Java.
6. Formatting and Escape Sequences
Text Blocks change how we handle escape sequences. We can still use \n or \t if we need to, but it’s usually not necessary.
The quote character (“) no longer needs to be escaped, which is a huge benefit. We only need to escape if we specifically need to have three or more quote characters in a row.
Text Blocks also introduce two new escape sequences for fine-grained formatting:
- \s (space escape) – Inserts a single space, even at the end of a line. It’s important because Java trims trailing whitespace in text block lines by default.
- \ (line continuation) – This is for when we have a very long line of code (like a SQL query) that we want to break in our source file, but do not want a newline in the final String. Ending a line with a backslash suppresses the actual newline.
Let’s add a test to show these escapes in action:
@Test
void whenUsingSpaceAndLineTerminatorEscapes_thenCorrect() {
String longSql = """
SELECT * FROM Task \
WHERE status = 'DONE' \
ORDER BY due_date DESC\s\s
""";
Stream<String> lines = longSql.lines();
Stream<String> expectedLines = Stream.of(
"SELECT * FROM Task WHERE status = 'DONE' ORDER BY due_date DESC "
);
assertLinesMatch(expectedLines, lines);
}
In this example, the \ escapes allow us to wrap long text in source code without introducing real line breaks, and \s preserves trailing space in text block lines.
7. New String Methods
The String class also gained a few new helper methods to work with text blocks and strings in general: stripIndent(), translateEscapes(), and formatted().
- stripIndent() – Applies the same “incidental whitespace” removal algorithm to any string, not just one from a text block. It helps clean up strings that we might get from other sources.
- translateEscapes() – Converts any literal escape sequences (like the text \\n) into their actual character values (like the newline character \n).
- formatted() – An instance method that works just like the static String.format(), making it very easy to use a text block as a template.
Next, let’s create some test methods to see these new methods in action.
7.1. The stripIndent() Method
Let’s first create a test method to demonstrate stripIndent() on a regular String variable:
@Test
void whenUsingStripIndent_thenCorrect() {
String indentedString = " {\n \"name\": \"Hello Baeldung\"\n }";
String stripped = indentedString.stripIndent();
Stream<String> linesAfterStrippedIndent = stripped.lines();
Stream<String> expectedLines = Stream.of(
"{",
" \"name\": \"Hello Baeldung\"",
"}"
);
assertLinesMatch(expectedLines, linesAfterStrippedIndent);
}
As we can see, even if indentedString is a regular String variable, instead of a text block, stripIndent() can still apply incidental whitespace removal to it.
7.2. The translateEscapes() Method
Let’s create another test method to understand how this method translates escapes:
@Test
void whenUsingTranslateEscapes_thenCorrect() {
String withEscapes = "Hello\\nWorld!\\tThis is a test.";
//before translateEscapes: single line
assertEquals(1, withEscapes.lines().count());
String translated = withEscapes.translateEscapes();
//after translateEscapes: multi line
assertEquals("Hello\nWorld!\tThis is a test.", translated);
Stream<String> expectedLines = Stream.of(
"Hello",
"World! This is a test."
);
assertLinesMatch(expectedLines, translated.lines());
}
Here, the translateEscapes() method converts escape sequences inside a string into their actual characters. However, it is not needed for Java text blocks, because text blocks automatically handle most escapes normally.
7.3. The formatted() Method
formatted() allows us to insert values into a string using format placeholders. As always, let’s create a test to demonstrate its usage:
@Test
void whenUsingFormatted_thenCorrect() {
String json = """
{
"name": "%s",
"status": "%s"
}
""".formatted("Learn Java Task", "DONE");
Stream<String> lines = json.lines();
Stream<String> expectedLines = Stream.of(
"{",
" \"name\": \"Learn Java Task\",",
" \"status\": \"DONE\"",
"}"
);
assertLinesMatch(expectedLines, lines);
}
As we can see, formatted() behaves like String.format(), but cleaner and easier to read. This is because we call formatted() directly on the string instance.
8. Conclusion
In this lesson, we explored Text Blocks, a great readability feature for Java.
We saw how the new “”” syntax eliminates the need for string concatenation, escaped newlines, and escaped quotes. We learned that indentation is handled automatically based on the position of the closing “”” quotes.