Was lange währt, wird endlich gut

Textblöcke in Java 13: Warum sich das lange Warten gelohnt hat

Tim Zöller

© HomeArt/Shutterstock.com

Lange Zeit mussten Java-Entwickler beim Formulieren von mehrzeiligen String-Literalen unschöne Umwege nehmen, während sie neidisch zu Scala, Kotlin, Groovy und anderen Sprachen schielten. Mit JEP 355 halten Textblöcke nun als Vorschau in Java 13 Einzug.

String-Literale in Java-Code zu schreiben, über mehrere Zeilen und unter Umständen mit Whitespace formatiert, ist für viele Entwickler ärgerlich. Um z. B. ein SQL Statement im Code übersichtlich über mehrere Zeilen zu formatieren, ist die Nutzung des +-Operators mit händisch eingefügten Zeilenumbrüchen am Ende üblich (Listing 1). Um festzustellen, dass die meisten Sprachen die Fähigkeit zu ordentlich formatierten String-Literalen bereits enthalten, muss man sich nur Groovy, Scala und Kotlin anschauen. Andere höhere Programmiersprachen wie C#, Swift oder Python stehen dem in nichts nach. Bei einer so hohen Anzahl an Vorbildern ist es möglich, die besten Eigenschaften der Umsetzungen für Java zu adaptieren. Das lässt sich am JEP 326 für Raw String Literals erkennen, ursprünglich für Java 12 vorgesehen. Die Community hatte viele Anmerkungen und Einwände, woraufhin der Vorschlag zurückgezogen wurde. JEP 355 hat viele der Anmerkungen berücksichtigt und ermöglicht nun Entwicklern ab Java 13, auf einfache Art Multi-line-Strings im Code zu definieren. Das Feature hält im JDK 13 zunächst als Vorschau Einzug – wer es nutzen möchte, muss seinen Code also mit dem Flag --enable-preview kompilieren.

String statement = "SELECT\n" +
                   "   c.id,\n" +
                   "   c.firstname,\n" +
                   "   c.lastName,\n" +
                   "   c.customernumber\n" +
                   "FROM customers c;";

JEP 326: Raw Strings

Um einige der Designentscheidungen für Text-Blocks besser einordnen zu können, lohnt sich ein Blick auf den zurückgezogenen JEP 326. Dieser hatte das JDK 12 als Ziel und sah vor, Raw Strings in Java einzuführen – Zeichenfolgen, die sich über mehrere Zeilen erstrecken können und keine Escape-Sequenzen interpretieren. Um möglichst flexibel und unabhängig vom enthaltenen Text funktionieren zu können, wurde als Begrenzungssequenz eine beliebige Anzahl von Backquotes (') vorgeschlagen. Enthält der String selbst einen Backquote, müsste die Begrenzungssequenz mindestens zwei enthalten. Enthält die Sequenz zwei aufeinanderfolgende Backquotes, hätte die Begrenzungssequenz zumindest drei usw. In der Ankündigung, dass der JEP zurückgezogen wird, erwähnt Brian Goetz einige Kritikpunkte aus der Community, die zu dieser Entscheidung geführt haben. Viele Entwickler befürchteten, die variable Anzahl an Zeichen in der Begrenzungssequenz könne für Menschen und Entwicklungsumgebungen verwirrend sein. Außerdem wurde Kritik daran laut, dass mit dem Backquote eines der wenigen unbenutzten Begrenzungszeichen im Code eingeführt wird, das damit „verbraucht“ wäre und für künftige Features nicht verwendet werden könnte. Auch die Tatsache, dass jedes mehrzeilige String-Literal mit dem vorgeschlagenen Feature automatisch auch ein Raw String sein musste, gab Anlass zu der Entscheidung, JEP 236 zurückzuziehen. Die in JEP 355 enthaltenen Ansätze bauen explizit auf den Erkenntnissen dieses Prozesses auf.

Java 13: Alle Features der neuen Version im Detail

Begrenzungszeichen und Formatierung

Mehrzeilige String-Literale werden von einer Sequenz aus drei Anführungszeichen (""") eingefasst. Die öffnende Zeichensequenz darf hierbei nur optional von Whitespaces und einem zwingend notwendigen Zeilenumbruch gefolgt werden. Der eigentliche Inhalt des Literals beginnt erst in der nächsten Codezeile (Listing 2). Einfache und doppelte Anführungszeichen dürfen im Literal vorkommen, ohne dass sie escapet werden müssen. Enthält das Literal selbst drei Anführungszeichen, muss mindestens eines von ihnen escapet werden.

String korrekterString = """ // Erstes Zeichen erst in nächster Zeile, korrekt
                         {
                           "typ": "json",
                           "inhalt": "Beispieltext"
                         }
                         """;

String inkorrekterString = """{ // Zeichen nach Begrenzungssequenz, inkorrekt
                                "typ": "json",
                                "inhalt": "Beispieltext"
                              }
                           """;

Mehrzeilige String-Literale werden zur Compile-Zeit verarbeitet. Dabei geht der Compiler immer nach demselben Schema vor: Sämtliche Zeilenumbrüche in der Zeichenfolge werden zunächst betriebssystemunabhängig in einen Line Feed (\u000A) umgewandelt. Davon ausgeschlossen sind explizit mit Escape-Zeichen im Text eingetragene Sequenzen wie \n oder \r.

Im zweiten Schritt wird der Whitespace entfernt, der der Codeformatierung geschuldet ist. Gekennzeichnet wird er mit der Position der abschließenden drei Anführungszeichen. So kann man Textblöcke im Code unterbringen, dass es zur restlichen Codeformatierung passt (Listing 3). Bei Scala und Kotlin müssen mehrzeilige Literale entweder linksbündig ohne Whitespace in den Quelltext geschrieben oder mit einer Stringmanipulation von formatierungsbedingtem Whitespace befreit werden. Java hingegen orientiert sich stark an der Vorgehensweise von Swift, eine Bereinigung der Strings zur Laufzeit ist nicht nötig.

Zuletzt werden alle Escape-Sequenzen im Text interpretiert und aufgelöst. Nach dem Kompilieren ist es nicht mehr möglich, herauszufinden, wie ein String im Code definiert wurde, ob er als Multi-line-String definiert war oder nicht.

String string1 = """
                 {
                   "typ": "json",
                   "inhalt": "Beispieltext"
                 }
                 """;	

// Inhalt von string1.
// Der linke Rand wird zur Veranschaulichung durch | markiert.
//|{
//|	"typ": "json",
//|	"inhalt": "Beispieltext"
//|}

String string2 = """
                 {
                   "typ": "json",
                   "inhalt": "Beispieltext"
                 }
          """;	

// Inhalt von string2. 
// Der linke Rand wird zur Veranschaulichung durch | markiert.
//|       {
//|	       "typ": "json",
//|        "inhalt": "Beispieltext"
//|       }

Anwendungsfälle

Mit den neuen Möglichkeiten, die Text-Blocks für Entwickler bieten, lässt sich Code in einigen Fällen sauberer und/oder lesbarer schreiben. Wie erwähnt, ist es nun z. B. möglich, SQL-Statements im Java-Code übersichtlicher darzustellen. Da Text-Blocks überall verwendet werden können, wo auch herkömmliche String-Literale erlaubt sind, gilt das sogar für Named Queries mit JQL oder nativem SQL (Listing 4), die innerhalb von Annotations definiert werden.

@Entity
@NamedQuery(name = "findByCustomerNumberAndCity",
            query = """
                    from Customer c
                    where c.customerNo = :customerNo
                    and c.city = :city
                    """)
public class CustomerEntity {

  String customerNo;
  String city;

  public String getCustomerNo() {
    return customerNo;
  }

  public void setCustomerNo(String customerNo) {
    this.customerNo = customerNo;
  }
}

Weitere sinnvolle Anwendungsfälle findet man, wenn man String-Literale als Templates nutzen möchte. Das kann entweder für JSON Payloads in Unit-Tests hilfreich sein, mit denen man einen REST-Service testen möchte, oder beim serverseitigen Vorbereiten von HTML Snippets (Listing 5).

String htmlContent = """
                     <div>
                       <h2>My header</h2>
                       <ul>
                         <li>An entry</li>
                         <li>Another entry</li>
                       </ul>
                     </div>
                     """;

Darüber hinaus vereinfachen Textblöcke die Nutzung von polyglotten Features deutlich, sei es mit der alten Nashorn-Engine oder mit dem Context der GraalVM. Der im Literal definierte JavaScript-Code wäre deutlich schlechter les- und wartbar, wenn das eingangs erwähnte Konstrukt mit Stringverkettung und manuellen Zeilenumbrüchen bemüht werden müsste (Listing 6).

ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object result = engine.eval("""
                            function add(int1, int2) {
                              return int1 + int2;
                            }
                            add(1, 2);""");

 

Neue Methoden

Im Rahmen von JEP 355 werden drei neue Instanzmethoden in der Klasse String hinzugefügt: formatted, stripIndent und translateEscapes. Während formatted eine Hilfsmethode ist, die die Arbeit mit mehrzeiligen Literalen erleichtert, bilden stripIndent und translateEscapes die beiden letzten Compile-Schritte bei deren Interpretation ab.

Wie erwähnt, eignen sich Multi-line-Strings hervorragend, um Templates im Code zu definieren. Um Platzhalter in Stringtemplates mit Werten zu befüllen, existiert bereits die statische Methode format in der Klasse String. Da sich mehrzeilig definierte String-Literale in der Benutzung von einzeiligen nicht unterscheiden, können sie natürlich ebenfalls mit dieser Methode formatiert werden. Um für Textblöcke eine übersichtlichere Herangehensweise zu ermöglichen, wurde der Klasse String die neue Instanzmethode formatted spendiert, die sich analog zur statischen Methode format verhält. Das Ergebnis ist ebenfalls ein mit Platzhaltern formatierter String, in Verbindung mit Textblöcken ergibt sich aber Code, der aufgeräumter ist (Listing 7)

String.format("Hallo, %s, %s dich zu sehen!", "Duke", "schön");

String xmlString = """
                   <customer>
                     <no>%s</no>
                     <name>%s</name>
                   </customer>
                   """.formatted("12345", "Franz Kunde");

Die Methode stripIndent entfernt in mehrzeiligen Strings vorangestellten Whitespace, den alle Zeilen gemein haben, rückt also den gesamten Text nach links, ohne die Formatierung zu verändern. Die Methode translateEscapes interpretiert sämtliche Escape-Sequenzen, die in einem String enthalten sind. In Listing 8 wird dieses Verhalten noch einmal verdeutlicht.

// Der linke Rand wird in der Ausgabe zur Veranschaulichung durch | markiert
String example = " a\n  b\\n   c";

System.out.println(example);
//| a
//|  b\n   c

System.out.println(example.stripIndent());
//|a
//| b\n   c

System.out.println(example.translateEscapes());
//| a
//|  b
//|   c

System.out.println(example.stripIndent().translateEscapes());
//|a
//| b
//|  c

Fazit

Mit JEP 355 werden mehrzeilige String-Literale auf eine Art und Weise eingeführt, die sich sowohl in ihrer Deklaration als auch in ihrer Formatierung schnell natürlich anfühlen. Die Entwickler haben hierbei von den Erfahrungen in anderen Programmiersprachen profitiert und sich für einen Weg entschieden, der das Nachbearbeiten von Strings, etwa um störenden Whitespace zu entfernen, unnötig macht und gleichzeitig eine leserliche Formatierung von Quelltext ermöglicht. Abgerundet wird die Einführung von drei neuen Methoden in der Klasse String: formatted, stripIndent und translateEscapes. Sie ermöglichen nicht nur einen einfacheren Umgang mit den neuen Textblöcken, sondern lassen sich auch nutzen, um Strings aus anderen Quellen zu formatieren, Einrückungen zu entfernen oder Escape-Sequenzen zu interpretieren. Ich persönlich freue mich sehr auf die Einführung von Textblöcken als vollwertiges Feature und merke bei meiner Arbeit fast täglich, wie sehr mir diese momentan fehlen.

Java 13 ist da – die Highlights in der Übersicht [mit Infografik]

Verwandte Themen:

Geschrieben von
Tim Zöller
Tim Zöller
Tim Zöller arbeitet als IT Consultant bei der ilum:e informatik AG in Mainz und entwickelt seit zehn Jahren Software. Er hilft Unternehmen dabei, ihre Prozesse mit Java zu digitalisieren und beschäftigt sich auch privat mit den Neuerungen in der Java-Welt. Er ist Mitgründer der Java User Group Mainz.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: