Java Text Blocks Explained — Cleaner Multi-line Strings in Java 15
Every Java developer has been there — you're embedding a chunk of HTML, a JSON payload, or a SQL query directly in your code, and within seconds your once-clean file looks like a bowl of spaghetti. String concatenation symbols, escaped quotes, and newline characters are scattered everywhere. It works, but nobody — including you two weeks later — can read it without squinting. This isn't a minor inconvenience; it's a genuine productivity killer that leads to real bugs when someone misplaces a quote or forgets a newline escape.
Text blocks were introduced as a preview feature in Java 13, refined in Java 14, and made a permanent, production-ready feature in Java 15. They solve one specific problem brilliantly: letting you write multi-line string literals in your Java source code in a way that looks exactly like the content you're trying to represent — HTML looks like HTML, JSON looks like JSON, SQL looks like SQL. No more escape gymnastics.
By the end of this article you'll understand exactly what a text block is, why it exists, how the indentation stripping works (this trips up almost everyone), and how to use new text block methods like stripIndent() and translateEscapes(). You'll write real, runnable examples and walk away with the confidence to use text blocks correctly in your own projects the very same day.
The Old Way: Why Multi-line Strings Were a Pain in Java
Before text blocks, the only way to write a multi-line string in Java was to use a regular string literal — which must start and end on a single line. That meant gluing lines together with the + operator and manually adding for line breaks. It works, but it's brutal to write and even harder to read.
Consider embedding a small JSON object inside your Java code. In real applications, you do this all the time — unit tests, API clients, mock data, configuration. The traditional approach forces you to mentally translate between what the string should look like and what the Java source code looks like. They're completely different.
The bigger the string, the worse the problem gets. A 10-line SQL query written the old way becomes a 10-line pile of escaped quotes and concatenation operators that makes code reviews painful and bugs easy to hide. Every \" you write is a chance to make a typo. Every + at the end of a line is visual noise that obscures the actual content you care about. Text blocks exist to remove all of that noise.
public class OldStyleStrings { public static void main(String[] args) { // The OLD way: a JSON object embedded as a traditional string. // Notice how unreadable this is — escaped quotes everywhere, // plus-signs to join lines, and \n to fake line breaks. String customerJson = "{\n" + " \"name\": \"Alice\",\n" + " \"age\": 30,\n" + " \"city\": \"Dublin\"\n" + "}"; // The same SQL query written the old way. // Good luck spotting a typo in here during a code review. String findActiveUsers = "SELECT user_id, email, created_at " + "FROM users " + "WHERE status = 'active' " + "AND created_at > '2024-01-01' " + "ORDER BY created_at DESC;"; System.out.println("--- JSON Output ---"); System.out.println(customerJson); System.out.println("\n--- SQL Output ---"); System.out.println(findActiveUsers); } }
{
"name": "Alice",
"age": 30,
"city": "Dublin"
}
--- SQL Output ---
SELECT user_id, email, created_at FROM users WHERE status = 'active' AND created_at > '2024-01-01' ORDER BY created_at DESC;
Text Block Syntax: How to Write One From Scratch
A text block starts with three double-quote characters (""" — called the opening delimiter), followed immediately by a newline. Your content then starts on the very next line. It ends with another three double-quote characters (the closing delimiter), which can sit on a line by itself or at the end of your last content line.
That mandatory newline after the opening """ is not optional — it's part of the syntax. If you try to write """some text""" on one line, Java will refuse to compile it. The language designers did this deliberately: a text block is meant for multi-line content, so at least one newline is required.
The most powerful thing about text blocks is that you write the content exactly as you want it to appear. No characters. No escaped quotes inside the content (double quotes are fine as-is, because the block only ends when it sees three consecutive quotes). The indentation is also handled automatically — we'll dig into exactly how that works in the next section, because it's the detail that catches nearly every beginner.
public class TextBlockBasics { public static void main(String[] args) { // TEXT BLOCK SYNTAX: // 1. Opening delimiter: three double-quotes then IMMEDIATELY a newline. // 2. Your content, written exactly as you want it to look. // 3. Closing delimiter: three double-quotes. // Example 1: A JSON object — looks like real JSON, not a string puzzle. String productJson = """ { "productId": "SKU-9921", "name": "Mechanical Keyboard", "price": 129.99, "inStock": true } """; // Example 2: An HTML snippet — notice, no escaped quotes needed for // the href attribute value. Text blocks handle double quotes natively. String welcomeHtml = """ <div class="welcome-banner"> <h1>Welcome back!</h1> <p>Your session started at 09:00 AM.</p> </div> """; // Example 3: A multi-line SQL query — readable like actual SQL. String topSellerQuery = """ SELECT p.name, SUM(o.quantity) AS total_sold FROM products p JOIN order_items o ON p.id = o.product_id WHERE p.category = 'electronics' GROUP BY p.name ORDER BY total_sold DESC LIMIT 10; """; System.out.println("=== JSON ==="); System.out.println(productJson); System.out.println("=== HTML ==="); System.out.println(welcomeHtml); System.out.println("=== SQL ==="); System.out.println(topSellerQuery); } }
{
"productId": "SKU-9921",
"name": "Mechanical Keyboard",
"price": 129.99,
"inStock": true
}
=== HTML ===
<div class="welcome-banner">
<h1>Welcome back!</h1>
<p>Your session started at 09:00 AM.</p>
</div>
=== SQL ===
SELECT p.name, SUM(o.quantity) AS total_sold
FROM products p
JOIN order_items o ON p.id = o.product_id
WHERE p.category = 'electronics'
GROUP BY p.name
ORDER BY total_sold DESC
LIMIT 10;
How Indentation Stripping Works — The Part Everyone Gets Wrong
Here's the part that confuses almost every developer who's new to text blocks. When you write a text block inside a method, you naturally indent the content to match the surrounding code. But you probably don't want all that leading whitespace to actually be part of the string. Java handles this automatically through a process called incidental whitespace stripping.
Java looks at every line inside the text block and finds the leftmost non-whitespace character across all of them (including the closing delimiter's position). That leftmost column is called the common indent prefix. Java strips that many leading spaces from every line. The result is a string whose indentation is relative — it only contains the spaces that are actually part of your content's internal structure, not the spaces you added to make your source code look neat.
The position of the closing """ is your control dial. Move it left and you preserve more indentation. Move it right and you strip more. If you place the closing delimiter at column zero (touching the left margin), Java strips ALL leading indentation. This gives you precise, predictable control over what ends up in the string — once you understand the rule.
public class IndentationStripping { public static void main(String[] args) { // SCENARIO A: Closing delimiter on its own line, aligned with content. // Java measures the shortest leading whitespace across all lines, // which here is 16 spaces (the indentation of the content lines). // Those 16 spaces are stripped from every line. // The internal 2-space indent of "city" is PRESERVED because it's // extra indentation BEYOND the common prefix. String addressBlock = """ Street: 42 Elm Road City: Springfield Country: Ireland """; System.out.println("SCENARIO A — standard stripping:"); System.out.println(addressBlock); // Prints with zero leading spaces. The 2 extra spaces before 'City' are kept. // SCENARIO B: Closing delimiter moved LEFT to column 0. // Now Java's minimum indent is 0, so NOTHING is stripped. // This means every content line keeps all its leading whitespace. // Usually NOT what you want — shown here for contrast. String withLeadingSpaces = """ Line one has 16 leading spaces Line two also has 16 leading spaces """; // Closing """ is at column 0 — all indentation preserved in the string. System.out.println("SCENARIO B — closing delimiter at column 0:"); // Each line now has 16 spaces of padding before its text. System.out.println("|" + withLeadingSpaces.lines().findFirst().orElse("") + "|"); // The pipe characters reveal the leading spaces clearly. // SCENARIO C: Using the indent() method to ADD indentation after the fact. // indent(4) adds 4 spaces to every line and ensures a trailing newline. String codeSnippet = """ if (loggedIn) { showDashboard(); } """; System.out.println("SCENARIO C — adding 4-space indent with indent():"); System.out.print(codeSnippet.indent(4)); } }
Street: 42 Elm Road
City: Springfield
Country: Ireland
SCENARIO B — closing delimiter at column 0:
| Line one has 16 leading spaces|
SCENARIO C — adding 4-space indent with indent():
if (loggedIn) {
showDashboard();
}
New String Methods and Real-World Text Block Patterns
Java 15 didn't just add text block syntax — it also added three new String methods designed to work alongside them. stripIndent() does the same incidental-whitespace removal that text blocks do automatically, but you can call it on any regular String at runtime. translateEscapes() processes escape sequences like \t and in a string that came from a file or user input, the same way the compiler does for string literals. And formatted() is a cleaner way to call String.format() — you call it directly on the text block itself, keeping substitution values right next to the template.
For real-world use, text blocks shine brightest in test code, configuration builders, template engines, and anywhere you're constructing structured text payloads. They make your unit tests dramatically more readable because the expected JSON or HTML sits right in front of you, not scrambled across multiple concatenated lines.
One important thing to remember: text blocks are still just String objects. There's no new type. Anything you can do with a String — pass it to a method, concatenate it, compare it with equals() — works exactly the same way.
public class TextBlockMethods { public static void main(String[] args) { // --- formatted(): inline variable substitution on a text block --- // formatted() is essentially String.format() called on the instance. // %s = insert a String, %d = insert an integer, %.2f = insert a float. String userName = "Maria"; int orderCount = 3; double totalAmount = 74.50; String orderSummaryEmail = """ Hi %s, Thanks for your order! Here's a quick summary: Orders placed : %d Total charged : $%.2f We'll email your receipt shortly. """.formatted(userName, orderCount, totalAmount); // .formatted() is called directly on the text block — clean and readable. System.out.println("=== Order Confirmation Email ==="); System.out.println(orderSummaryEmail); // --- stripIndent(): same logic as text blocks, but for runtime strings --- // Imagine reading indented text from a file or another source. // stripIndent() removes the common leading whitespace. String rawTextFromFile = " SELECT id\n" + " FROM sessions\n" + " WHERE active = true;\n"; String cleanedQuery = rawTextFromFile.stripIndent(); System.out.println("=== stripIndent() result ==="); System.out.println(cleanedQuery); // --- translateEscapes(): interprets \t, \n etc in a literal string --- // Useful when escape sequences arrive as raw text (e.g., from a database // or config file) and need to be processed into actual characters. String rawEscapes = "Column1\\tColumn2\\tColumn3\\nRow1\\tRow2\\tRow3"; // Right now \t is literally backslash-t, not a tab character. String processedTable = rawEscapes.translateEscapes(); // After translateEscapes(), \t becomes a real tab, \n a real newline. System.out.println("=== translateEscapes() result ==="); System.out.println(processedTable); // --- Text blocks are just String objects — prove it --- String textBlockGreeting = """ Hello, World! """; String regularGreeting = "Hello, World!\n"; // equals() compares content — these two strings are identical. boolean contentMatches = textBlockGreeting.equals(regularGreeting); System.out.println("\nText block equals regular String: " + contentMatches); } }
Hi Maria,
Thanks for your order! Here's a quick summary:
Orders placed : 3
Total charged : $74.50
We'll email your receipt shortly.
=== stripIndent() result ===
SELECT id
FROM sessions
WHERE active = true;
=== translateEscapes() result ===
Column1 Column2 Column3
Row1 Row2 Row3
Text block equals regular String: true
| Aspect | Traditional String Literal | Text Block (Java 15+) |
|---|---|---|
| Syntax start | Double quote: " | Three double quotes + newline: """\n |
| Multi-line support | Manual \n + concatenation with + | Native — just press Enter in your editor |
| Embedded double quotes | Must escape every one: \" | No escaping needed — write " directly |
| Readability of JSON/HTML/SQL | Very poor — looks nothing like the content | Excellent — source mirrors actual content |
| Trailing newline control | You control it explicitly | Controlled by closing delimiter position |
| Indentation control | Every space is literal | Automatic stripping of common indent prefix |
| Runtime type | java.lang.String | java.lang.String (identical type) |
| Available since | Java 1.0 | Preview in Java 13–14, standard in Java 15 |
| Variable substitution | String.format("...", values) | textBlock.formatted(values) — inline style |
| Use in switch/var/return | Yes | Yes — it's just a String |
🎯 Key Takeaways
- Text blocks require the opening """ to be immediately followed by a newline — content always starts on the next line, no exceptions.
- Incidental whitespace stripping removes the common leading indent automatically; the closing delimiter's column position is your precise control over how much is stripped.
- Text blocks compile to the same java.lang.String type — there's no new class, no performance difference, and no API changes needed downstream.
- Use .formatted() directly on a text block for clean, inline variable substitution — it keeps the template and its values visually together, unlike a wrapping String.format() call.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Putting content on the same line as the opening delimiter — Writing """{ \"name\": \"Alice\" }""" (opening and content on one line) causes a compile error: 'illegal text block open delimiter sequence'. Fix: the opening """ must always be followed by a newline before any content starts. Think of the opening delimiter as a door — you open it, then step through to the next line.
- ✕Mistake 2: Being surprised by unexpected leading spaces because the closing delimiter is placed at column zero — If you put the closing """ at the leftmost column of the file, Java measures the common indent as zero, so none of the leading whitespace is stripped. Every content line keeps all its indentation, which usually gives you a string bloated with spaces you didn't intend. Fix: align the closing """ with the leftmost character of your content, or use .stripIndent() after the fact.
- ✕Mistake 3: Expecting text blocks to handle expression interpolation like other languages — Developers coming from Kotlin, Python, or JavaScript expect to write something like """Hello ${userName}""" and have it substitute the variable. Java text blocks don't do this. Fix: use .formatted() with %s/%d placeholders, or concatenate the text block result with + like any other String. Groovy/Kotlin-style interpolation is not part of the Java text block spec.
Interview Questions on This Topic
- QWhat is a text block in Java 15 and how is it different from a regular String literal? Can you show an example where a text block is clearly the better choice?
- QExplain how incidental whitespace stripping works in text blocks. How does the position of the closing '"""' affect the final string content?
- QWhat are the three new String methods added alongside text blocks in Java 15? Describe what stripIndent() does that the text block syntax doesn't already do automatically — when would you actually need it?
Frequently Asked Questions
Can I use text blocks in Java 11 or Java 8?
No. Text blocks were introduced as a preview feature in Java 13, updated in Java 14, and became a permanent standard feature in Java 15. If your project is on an older JDK you'll need to use traditional string concatenation or upgrade your Java version to use text blocks.
Do text blocks add any performance overhead compared to regular strings?
None at all. The Java compiler processes text blocks at compile time and produces exactly the same bytecode as an equivalent regular String literal. At runtime there is zero difference — a text block is just a String.
Do I need to escape double quotes inside a text block?
Almost never. A text block ends only when it encounters three consecutive double-quote characters, so one or two double quotes inside your content require no escaping. The only edge case is if you need three or more consecutive quotes inside the content — in that case, escape at least one of them with a backslash: \""" to break the sequence.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.