Text Blocks und neue String API (Java 15+)
1. Übersicht
Vor Java 15 war es ziemlich mühsam, mehrzeilige JSON-, SQL- oder HTML-Abschnitte im Java-Code zu schreiben. Du musstest Strings mit + verketten, \n hinzufügen und jedes Anführungszeichen " escapen. Text Blocks (offiziell seit Java 15, JEP 378) lösen dieses Problem. Zusammen mit ihnen kommen einige neue Methoden in String.
Nach dem Lesen dieses Artikels kannst du Text Blocks mit korrekter Einrückung schreiben und die neuen Escape-Sequenzen \ und \s verwenden. Du wirst auch wissen, wann du formatted(), stripIndent(), translateEscapes() und einige zusätzliche APIs bis Java 21 einsetzen solltest.
2. Vorbereitung
Alle Beispiele laufen auf Java 21. Die Tests verwenden JUnit 5 und AssertJ:
<properties>
<maven.compiler.release>21</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<scope>test</scope>
</dependency>
</dependencies>
Wenn du Spring Boot 3.x verwendest, sind beide Bibliotheken bereits in spring-boot-starter-test enthalten.
3. Grundlagen von Text Blocks
3.1. Syntax
Ein Text Block wird mit drei Anführungszeichen """ geöffnet und dann sofort umgebrochen. Er wird mit """ beendet:
public class TextBlockDemo {
public static void main(String[] args) {
String json = """
{
"id": 42,
"customer": "Lan",
"status": "PAID"
}
""";
System.out.print(json);
}
}
Ausgabe:
{
"id": 42,
"customer": "Lan",
"status": "PAID"
}
Dieser Code hat ein paar wichtige Punkte zu beachten:
- Anführungszeichen
"im Inneren müssen nicht escaped werden. - Der gemeinsame Einzug am Anfang jeder Zeile wird entfernt. Dies wird incidental whitespace genannt. Der relative Einzug, wie 2 Leerzeichen vor
"id", bleibt erhalten. - Da das schließende
"""auf einer eigenen Zeile steht, endet der String mit\n.
Ein Text Block ist ein normaler String. Er wird vollständig beim Kompilieren verarbeitet, sodass keine zusätzlichen Laufzeitkosten entstehen.
Hinweis: Du darfst keinen Inhalt direkt nach dem öffnenden
"""schreiben. Die ZeileString s = """Hello""";führt zu einem Compilerfehler: illegal text block open delimiter sequence.
3.2. Position des schließenden """ bestimmt die Einrückung
Der Compiler sucht die Zeile mit dem wenigsten Leerzeichen am Anfang, **einschließlich der Zeile mit dem schließenden """ **. Dann entfernt er genau diese Anzahl von Leerzeichen aus jeder Zeile. Daher kannst du absichtlich Einrückung hinzufügen, indem du das schließende """ nach links verschiebst:
String html = """
<ul>
<li>Bestellung #1</li>
</ul>
""";
System.out.print(html);
<ul>
<li>Bestellung #1</li>
</ul>
Das schließende """ ist um 4 Spalten weniger eingerückt als der Inhalt, daher behält jede Zeile 4 Leerzeichen am Anfang.
Wenn du nicht möchtest, dass das Zeichen \n am Ende der Zeichenkette steht, platziere """ direkt nach dem Inhalt:
String greeting = """
Hallo Lan""";
System.out.println("[" + greeting + "]");
[Hallo Lan]
3.3. Leerzeichen am Zeilenende und Zeilenumbruchzeichen
Es gibt zwei weitere Regeln, die du kennen musst:
- Leerzeichen am Ende jeder Zeile werden immer entfernt.
- Alle Arten von Zeilenumbrüchen (
\r\n,\r) werden zu\nnormalisiert. Dies gilt auch, wenn die Quelldatei mit Windows-Zeilenumbrüchen gespeichert ist.
Regel 2 sorgt für konsistente Ergebnisse zwischen verschiedenen Betriebssystemen. Wenn dein Protokoll jedoch \r\n verlangt, wie z.B. HTTP-Header oder CSV-Dateien nach RFC 4180, musst du das Zeilenumbruchzeichen selbst ersetzen.
4. Zwei neue Escape-Sequenzen: \<Zeilenumbruch> und \s
4.1. Zeilen mit \ verbinden
Ein langer SQL-Satz wird üblicherweise auf mehrere Zeilen verteilt, um ihn leichter lesbar zu machen. Manchmal brauchst du ihn aber auf einer einzigen Zeile, etwa beim Logging. Ein Backslash \ am Zeilenende verhindert den Zeilenumbruch:
String sql = """
SELECT id, total \
FROM orders \
WHERE status = 'PAID'""";
System.out.println(sql);
SELECT id, total FROM orders WHERE status = 'PAID'
Leerzeichen vor dem \ werden beibehalten. Der Grund ist, dass sie nicht mehr am Zeilenende stehen: das Zeichen \ kommt danach.
4.2. Leerzeichen mit \s bewahren
\s entspricht einem Leerzeichen (U+0020). Du verwendest es, um Leerzeichen am Zeilenende zu bewahren, die normalerweise gelöscht würden, z.B. beim Erstellen von Daten mit fester Breite:
String report = """
Lan \s
Minh \s
""";
report.lines().forEach(line -> System.out.println("[" + line + "]"));
[Lan ]
[Minh ]
Jede Zeile ist genau 8 Zeichen lang. Die vier Leerzeichen vor \s werden beibehalten, da sie nicht mehr am Zeilenende stehen.
4.3. Wenn der Inhalt """ enthält
Zwei aufeinanderfolgende Anführungszeichen " sind kein Problem. Drei aufeinanderfolgende Zeichen würden als Abschluss des Text Blocks interpretiert, daher musst du mindestens ein Zeichen escapen:
String doc = """
Python docstring nutzt \""" zum Öffnen und Schließen.
""";
5. Neue begleitende String API
Java 15 fügt drei Methoden zu String hinzu, um Text Blocks zu unterstützen. Dies sind auch die Schritte, die der Compiler bei der Verarbeitung von Text Blocks ausführt.
5.1. formatted()
formatted(Object... args) funktioniert wie String.format(this, args), aber du kannst es direkt auf der Zeichenkette aufrufen. Diese Schreibweise passt besonders gut zu Text Blocks:
record Order(long id, String customer, double total) {}
Order order = new Order(42, "Lan", 199_000);
String message = """
Bestellung #%d
Kunde: %s
Gesamtbetrag: %,.0f VND
""".formatted(order.id(), order.customer(), order.total());
System.out.print(message);
Bestellung #42
Kunde: Lan
Gesamtbetrag: 199,000 VND
Das Tausendertrennzeichen hängt von der Standard-Locale der JVM ab. Wenn du ein festes Ergebnis brauchst, verwende String.format(Locale.US, ...), da formatted() keinen Locale-Parameter akzeptiert.
5.2. stripIndent()
Diese Methode wendet den gleichen Algorithmus zur Entfernung von incidental whitespace wie Text Blocks auf eine beliebige Zeichenkette an. Sie ist nützlich, wenn mehrzeilige Zeichenketten aus Dateien oder Datenbanken stammen, anstatt im Quellcode zu stehen:
String raw = " SELECT *\n FROM customers\n ";
System.out.print(raw.stripIndent());
SELECT *
FROM customers
Die letzte Zeile enthält nur Leerzeichen, daher wird sie bei der Bestimmung der Einrückung berücksichtigt und wird zu einem leeren String. Dies ist genau die Rolle des schließenden """ in einem Text Block.
5.3. translateEscapes()
Diese Methode konvertiert Escape-Sequenzen in Zeichenform zu echten Zeichen. Du brauchst sie, wenn du Templates aus Konfigurationsdateien liest, die Strings wie \n oder \t enthalten:
String fromConfig = "Hallo\\tLan\\nDanke für deine Bestellung";
System.out.println(fromConfig.translateEscapes());
Hallo Lan
Danke für deine Bestellung
Falsch formatierte Escape-Sequenzen, wie z.B. \q, führen dazu, dass die Methode IllegalArgumentException wirft. Rufe sie nicht auf Benutzereingaben auf, ohne die Exception zu fangen. Hinweis: Diese Methode verarbeitet \u nicht, da Unicode-Escapes bereits vom Compiler übersetzt werden.
6. Andere hilfreiche Methoden (Java 11 – 21)
Viele kleine Methoden wurden in verschiedenen Versionen hinzugefügt. Sie werden oft zusammen mit Text Blocks verwendet:
String text = """
Lan
\s
Minh
""";
// Java 11
System.out.println(" ".isBlank()); // true
System.out.println(text.lines().filter(l -> !l.isBlank()).count()); // 2
System.out.println("\u2003Lan\u2003".strip()); // "Lan" (strip versteht Unicode, trim nicht)
System.out.println("-".repeat(10)); // ----------
// Java 12
System.out.print("Lan\nMinh".indent(2)); // fügt 2 Leerzeichen pro Zeile hinzu, endet mit \n
int length = "Lan".transform(String::length); // 3
// Java 21
System.out.println("order-42-paid".indexOf('-', 6, 13)); // 8, sucht nur in [6, 13)
System.out.println(Arrays.toString("a,b;c".splitWithDelimiters("[,;]", 0)));
// [a, , , b, ;, c]
Ein paar Details sind erwähnenswert:
indent(n)fügt n Leerzeichen am Anfang jeder Zeile hinzu, wennn > 0, oder entfernt maximal n Leerzeichen, wennn < 0. Es normalisiert immer Zeilenumbrüche und fügt\nam Ende hinzu.transform(Function)ermöglicht dir, String-Transformationsschritte auf natürliche Weise zu verketten, z.B.template.transform(this::render).transform(String::strip).- Ab Java 21 hat auch
StringBuilderdierepeat(...)-Methode:new StringBuilder().repeat("=", 5)ergibt=====.
7. Text Blocks in Spring Boot verwenden
In Spring Boot 3.x-Anwendungen eignen sich Text Blocks besonders gut für @Query von Spring Data JPA. Du kannst mehrzeilige Abfragen beibehalten, ohne Strings verketten zu müssen:
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("""
SELECT o FROM Order o
WHERE o.customer.email = :email
AND o.status = 'PAID'
ORDER BY o.createdAt DESC
""")
List<Order> findPaidOrders(@Param("email") String email);
}
Text Blocks sind Compile-Zeit-Konstanten und können daher als Annotationswert verwendet werden. Parameter werden immer noch über :email gebunden und sind sicher vor SQL-Injection.
8. Tests für das Verhalten von Text Blocks schreiben
Die Regeln für Einrückung und Zeilenumbruchzeichen sind leicht zu verwechseln. Daher solltest du Tests schreiben, um das gewünschte Verhalten festzulegen:
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class TextBlockTest {
@Test
void givenClosingDelimiterOnOwnLine_thenEndsWithNewline() {
String json = """
{
"id": 42
}
""";
assertThat(json).isEqualTo("{\n \"id\": 42\n}\n");
}
@Test
void givenBackslashAtLineEnd_thenLinesAreJoined() {
String sql = """
SELECT id \
FROM orders""";
assertThat(sql).isEqualTo("SELECT id FROM orders");
}
@Test
void givenSpaceEscape_thenTrailingSpacesArePreserved() {
String padded = """
Lan \s""";
assertThat(padded).hasSize(6).endsWith(" ");
}
@Test
void givenTemplate_whenFormatted_thenPlaceholdersAreReplaced() {
String message = """
Hallo %s, Bestellung #%d bezahlt.""".formatted("Lan", 42);
assertThat(message).isEqualTo("Hallo Lan, Bestellung #42 bezahlt.");
}
}
Alle vier Tests werden erfolgreich ausgeführt. Der erste Test ist am wichtigsten: er erinnert dich daran, dass das schließende """ auf einer eigenen Zeile das \n am Ende der Zeichenkette erzeugt.
9. Häufige Fehler und wann man Text Blocks nicht verwenden sollte
Tabs und Leerzeichen mischen. Bei der Berechnung von incidental whitespace behandelt der Compiler einen Tab als ein Leerzeichen. Wenn Zeilen mit zwei verschiedenen Arten eingerückt sind, ist das Ergebnis falsch, auch wenn es in der IDE richtig aussieht. Konfiguriere deinen Editor, um nur Leerzeichen zu verwenden.
\n am Ende vergessen. Beim Vergleich oder Verkettung von Strings führt das überschüssige \n oft zu Fehlern wie “sieht gleich aus, aber equals ist false”. Wenn du es nicht brauchst, platziere """ direkt nach dem Inhalt oder rufe strip() auf.
formatted() zum Verketten von SQL mit Benutzerdaten verwenden. Text Blocks machen Code nur lesbarer, nicht sicherer.
// FALSCH: SQL-Injection-Lücke
String sql = """
SELECT * FROM customers WHERE email = '%s'
""".formatted(email);
Verwende stattdessen PreparedStatement, JdbcTemplate mit ?-Parametern oder @Query mit named parameters.
Hinweis: Für JSON eignen sich Text Blocks für Test-Daten oder fest codierte Payloads. Wenn die Payload von dynamischen Daten abhängt, verwende Jackson zur Serialisierung. Manuelles String-Verketten escaped keine Sonderzeichen wie
"oder\in Kundennamen.
Kurze einzeilige Strings. "Lan" ist immer noch besser als ein dreizeiliger Text Block. Verwende Text Blocks nur, wenn der Inhalt mehrere Zeilen hat oder viele Anführungszeichen enthält.
Zu große Templates. Eine HTML-E-Mail mit mehreren hundert Zeilen in einer Java-Klasse macht den Code schwer zu warten. In diesem Fall solltest du sie in eine Template-Datei verschieben, z.B. mit Thymeleaf oder als Ressource in src/main/resources.
10. Fazit
Text Blocks ermöglichen es dir, JSON, SQL und HTML in Java kompakt und lesbar zu schreiben, wie in ihrer ursprünglichen Form. Du musst nur ein paar Regeln beachten:
- Das öffnende
"""muss sofort umgebrochen werden. - Die Position des schließenden
"""bestimmt die Einrückung und ob die Zeichenkette mit\nendet. - Leerzeichen am Zeilenende werden entfernt. Zeilenumbrüche werden immer zu
\nnormalisiert. - Verwende
\, um Zeilen zu verbinden, und\s, um Leerzeichen zu bewahren.
Bezüglich der API ist formatted() ideal zum Einfügen von Werten in Text Blocks. stripIndent() und translateEscapes() ermöglichen dir, die gleiche Logik auf externe Zeichenketten anzuwenden. Methoden wie lines(), strip(), indent(), transform() und splitWithDelimiters() in Java 21 helfen dir, mehrzeilige Strings ohne externe Bibliotheken zu verarbeiten. Abschließend solltest du Tests schreiben, um das gewünschte Verhalten zu dokumentieren, und verwende Text Blocks nicht zum manuellen Verketten von SQL mit Benutzerdaten.