JAX Blog

Fit für die Zukunft

Neues in Java 21: JEP 430 und 431

Oct 4, 2023

Java 21 bringt viele Verbesserungen in Syntax und APIs. In diesem Beitrag stehen die beiden Neuerungen JEP 430: „String Templates (Preview)“ sowie JEP 431: „Sequenced Collections“ im Fokus. Durch diese Funktionalitäten wird Java komfortabler und angenehmer in der Handhabung und besser für die Zukunft gerüstet. String Templates adressieren das Aufbereiten textueller Informationen, die aus fixen und variablen Bestandteilen bestehen, und Sequenced Collections vereinfachen den Zugriff und die Verwaltung von Daten am Anfang und Ende einer Collection.

Regelmäßig müssen Strings aus fixen und variablen Textbausteinen zusammengesetzt werden. Um das zu tun, gibt es verschiedene Varianten, angefangen bei der einfachen Konkatenation mit + bis hin zur formatierten Aufbereitung. Die mit Java 21 neu eingeführten String Templates (JEP 430) ergänzen die bisherigen Varianten um eine elegante Möglichkeit, Ausdrücke angeben zu können, die zur Laufzeit ausgewertet und passend in den String integriert werden. Allerdings sind die String Templates noch ein Preview-Feature und müssen beim Kompilieren und Ausführen explizit aktiviert werden.

Bisherige Vorgehensweise

Schauen wir kurz zurück. Um formatierte Ausgaben in Java zu erzeugen, verwenden viele Entwickler die folgenden Varianten, um aus Variablen und Textbausteinen zu kombinieren:

String result = "Calculation: " + x + " plus " + y + " equals " + (x + y);
System.out.println(result);

und

String resultSB = new StringBuilder().append("Calculation: ").append(x).append(" plus ").append(y).append(" equals ").append(x + y).toString();
System.out.println(resultSB);

Die Stringkonkatenation mit + ist leicht verständlich und oft auch durchaus lesbar. Die Lesbarkeit nimmt jedoch bei einer zunehmenden Anzahl von Verknüpfungen ab. Noch dramatischer ist der Effekt beim Einsatz eines StringBuffers oder StringBuilders und dessen Methode append(): Je mehr Elemente zu verknüpfen sind, desto unleserlicher und schwieriger nachvollziehbar wird das Konstrukt.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Weitere, in der Praxis seltener anzutreffende Möglichkeiten sind die Methoden format() und formatted() aus der Klasse String. Beide nutzen die gleichen Platzhalter, variieren aber ein wenig in der Handhabung, wobei meine Präferenz bei formatted() liegt.

var resultF1 = String.format("Calculation: %d plus %d equals %d", x, y, x + y);
System.out.println(resultF1);

var resultF2 = "Calculation: %d plus %d equals %d".formatted(x, y, x + y);
System.out.println(resultF2);

Darüber hinaus existiert noch die Klasse java.text.MessageFormat. Von ihr werden zunächst per Konstruktoraufruf mit fixem Text sowie integrierten Platzhaltern Instanzen erzeugt und danach mit format() befüllt – sowohl die Platzhalter als auch die Angabe der Werte unterscheiden sich von den beiden vorherigen Varianten:

var msgFormat = new MessageFormat("Calculation: {0} plus {1} equals {2}");
System.out.println(msgFormat.format(new Object[] { x, y, x + y }));

Möglichkeiten anderer Programmiersprachen

Viele Programmiersprachen unterstützten alternativ zur Stringkonkatenation die sogenannte Stringinterpolation oder auch formatierte Strings. Dabei kommt ein String zum Einsatz, in den an verschiedenen Positionen speziell gekennzeichnete Platzhalter integriert sind, die dann zur Laufzeit durch die entsprechenden Werte ersetzt werden. Das gilt beispielsweise für die f-Strings in Python. Etwas Ähnliches bieten Kotlin, Swift und C# durch unterschiedliche Notationen mit Platzhaltern:

  • Python: “Calculation: {x} + {y} = {x + y}”
  • Kotlin: “Calculation: $x + $y = ${x + y}”
  • Swift: “Calculation: \(x) + \(y) = \(x + y)”
  • C#: $”Calculation: {x} + {y}= {x + y}”

In allen Fällen werden die Platzhalter im String durch die entsprechenden Werte der Variablen, insbesondere auch von Berechnungen, ersetzt.

Die Alternative: String Templates (Preview)

Mit JEP 430 werden String Templates eingeführt, die wir uns nun anschauen wollen. Bilden wir das einführende Beispiel damit nach – aber bitte bedenken Sie, dass es sich um ein Preview-Feature handelt, das extra aktiviert werden muss, etwa wie folgt für die JShell:

$ jshell --enable-preview
|  Willkommen bei JShell - Version 21
|  Geben Sie für eine Einführung Folgendes ein: /help intro

Lernen wir zunächst die grundlegende Syntax kennen. Dabei muss dem String Template das Kürzel STR vorangestellt werden. Es aktiviert einen sogenannten Template Processor, der dann die Platzhalter im Format \{expression} ersetzt, im einfachsten Fall einfache Variablennamen durch deren Wert (Listing 1).

jshell> int x = 47
x ==> 47

jshell> int y = 11
y ==> 11

jshell> System.out.println(STR."Calculation: \{x} + \{y} = \{x + y}")
Calculation: 47 + 11 = 58

Bei der Angabe der Ausdrücke ist man nicht auf einfache mathematische Operationen beschränkt, sondern es lassen sich beliebige Java-Aufrufe einfügen, also auch Methoden (Listing 2).

jshell> int x = 7
x ==> 7

jshell> int y = 2
y ==> 2

jshell> STR."Berechnung: \{x} mal \{y} = \{Math.multiplyExact(x, y)}"
$3 ==> "Berechnung: 7 mal 2 = 14"

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Praxistipp

Bei der Einführung gab es diverse Diskussionen über das Format der Ausdrücke. Weil viele Third-Party-Bibliotheken etwa $, # oder {} dafür nutzen, hat man sich für ein Format entschieden, das nur innerhalb von String Templates gültig ist. Interessanterweise ist das STR per Definition public static final und automatisch in jeder Java-Datei bereits vorhanden.

Praktischerweise kann man String Templates auch in Kombination mit Textblöcken nutzen (Listing 3).

jshell> int statusCode = 201
statusCode ==> 201

jshell> var msg = "CREATED"
msg ==> "CREATED"

jshell> String json = STR."""
  ...>     {
  ...>       "statusCode": \{statusCode},
  ...>       "msg": "\{msg}"
  ...>     }""";
  ...> 

json ==> "{\n  \"statusCode\": 201,\n  \"msg\": \"CREATED\"\n}"

Tatsächlich gefällt mir in dem Zusammenhang die Methode formatted() ziemlich gut, und ich würde sie String Templates sogar vorziehen:

jshell> String json = STR."""
  ...> {
  ...>     "statusCode": %d,
  ...>     "msg": "%s"
  ...> }""".formatted(statusCode, msg);
json ==> "{\n    \"statusCode\": 201,\n    \"msg\": \"CREATED\"\n}"

String Templates lassen sich auch zur Aufbereitung einer simplen HTML-Seite nutzen (Listing 4). Nach dem Ersetzen entsteht das HTML in Listing 5.

String title = "My First Web Page";
String text = "My Hobbies:";
var hobbies = List.of("Cycling", "Hiking", "Shopping");
String html = STR."""
<html>
  <head><title>\{title}</title></head>
  <body>
    <p>\{text}</p>
    <ul>
      <li>\{hobbies.get(0)}</li>
      <li>\{hobbies.get(1)}</li>
      <li>\{hobbies.get(2)}</li>
    </ul>
  </body>
</html>""";
<html>
  <head><title>My First Web Page</title></head>
  <body>
    <p>My Hobbies:</p>
    <ul>
      <li>Cycling</li>
      <li>Hiking</li>
      <li>Shopping</li>
    </ul>
  </body>
</html>

Speichert man das Ganze als Datei und öffnet diese in einem Browser, so erhält man in etwa die Darstellung in Abbildung 1.

Abb. 1: Simple HTML-Seite mit String Templates

Besonderheiten

Interessanterweise ist es möglich, auch komplexere Aktionen in einem Platzhalter auszuführen. Als eher einfaches Beispiel haben wir bereits einen Methodenaufruf gesehen. Es sind aber auch Aktionen wie die in Listing 6 möglich, also ein Postinkrement, der ?-Operator oder Zugriffe auf das Date and Time API – in diesem Muster lassen sich dann einfache Anführungszeichen ohne Escaping nutzen.

int index = 0;
var modified = STR."\{index++}, \{index++}, \{index++}, \{index++}";
System.out.println(modified);

String filePath = "tmp.dat";
File file = new File(filePath);
  String old = "The file " + filePath + " " + file.exists() ? "does" : "does not" + " exist");
 String msg = STR. "."The file \{filePath} \{file.exists() ? 
                  "does" : "does not"} exist";

String currentTime = STR."Current time: \{DateTimeFormatter.ofPattern("HH:mm").format(LocalTime.now())}";
System.out.println(currentTime);

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Neben STR existieren weitere vordefinierte Prozessoren, etwa FMT, um eine Ausgabe wie mit String.format() zu erzielen. Betrachten wir dazu die Methode in Listing 7.

private static void alternativeStringProcessors() {
  int x = 47;
  int y = 11;
  String calculation1 = FMT."%6d\{x} + %6d\{y} = %6d\{x + y}";
  System.out.println("fmt calculation 1: " +calculation1);

  float base = 3.0f;
  float addon = 0.1415f;

  String calculation2 = FMT."%2.4f\{base} + %2.4f\{addon}" +
                        FMT."= %2.4f\{base + addon}";
  System.out.println("fmt calculation 2 " + calculation2);

    String calculation3 = FMT."Math.PI * 1.000 = %4.6f\{Math.PI * 1000}";
    System.out.println("fmt calculation 3 " + calculation3);
}

Als Ausgabe erhält man:

fmt calculation 1:     47  +     11 =     58
fmt calculation 2: 3.0000  + 0.1415 = 3.1415
fmt calculation 3: Math.PI * 1.000 = 3141.592654

Während STR standardmäßig verfügbar ist, muss man FMT auf geeignete Weise importieren:

import static java.util.FormatProcessor.FMT;

Eigene Stringprozessoren

Die Arbeitsweise von STR und FMT ist eingängig. Möchte man selbst steuernd eingreifen, ist es sogar möglich, eigene Template Processors zu erstellen, indem man das Interface StringTemplate.Processor implementiert. Dabei existieren diverse Methoden, die man für eine angepasste Variante nutzen kann. Folgendes Beispiel zeigt sowohl STR als auch die eigene Ausprägung in Form der Klasse MyProcessor im Einsatz:

String name = "Michael";
int age = 52;
System.out.println(STR."Hello \{name}. You are \{age} years old.");

var myProc = new MyProcessor();
System.out.println(myProc."Hello \{name}. You are \{age} years old.");

Betrachten wir die Ausgaben, die auch verschiedene Resultate der internen Methoden zeigen, um das Verständnis dafür zu erhöhen, wie die Abläufe und Verarbeitungsschritte innerhalb eines Template Processor erfolgen. Ziel des eigenen Prozessors ist es, die Werte jeweils speziell mit >> und << zu markieren:

Hello Michael. You are 52 years old.
-- process() --
=> fragments: [Hello , . You are ,  years old.]
=> values: [Michael, 52]
=> interpolate: Hello Michael. You are 52 years old.
Hello >>Michael<<. You are >>52<< years old.

Tatsächlich ist gar nicht viel Arbeit in der Methode process() zu erledigen, insbesondere würde man die Ausgaben weglassen, hier dienen sie lediglich dem Verständnis und der leichteren Nachvollziehbarkeit (Listing 8).

static class MyProcessor implements StringTemplate.Processor<String,
                                    IllegalArgumentException> {
    @Override
    public String process(StringTemplate stringTemplate) 
                  throws IllegalArgumentException {
        System.out.println("\n-- process() --");
        System.out.println("=> fragments: " + stringTemplate.fragments());
        System.out.println("=> values: " + stringTemplate.values());
        System.out.println("=> interpolate: " + stringTemplate.interpolate());
        System.out.println();

        var adjustedValues = stringTemplate.values().stream().
                                            map(str -> ">>" + str + "<<").
                                            toList();

        return StringTemplate.interpolate(stringTemplate.fragments(),
                                          adjustedValues);
    }
}

JEP 431: Sequenced Collections

Das Java Collections API ist eins der ältesten und am besten konzipierten APIs im JDK und bietet die drei Haupttypen List, Set und Map. Was allerdings fehlt, ist so etwas wie eine geordnete Reihenfolge von Elementen in Form eines Typs. Einige Collections haben eine sogenannte Encounter Order/Iteration Order (Begegnungsreihenfolge), d. h., es ist definiert, in welcher Reihenfolge die Elemente durchlaufen werden, etwa

  • List: indexbasiert, von vorne nach hinten
  • TreeSet: indirekt durch Comparable oder übergebenen Comparator definiert
  • LinkedHashSet: gemäß der Einfügereihenfolge

Dagegen definiert etwa HashSet keine derartige Encounter Order – und auch HashMap tut das nicht.

 

Auf Basis der Encounter Order ist auch das erste bzw. letzte Element definiert. Zudem lässt sich so eine Append-Funktionalität für vorn und hinten realisieren. Darüber hinaus ergibt sich auch die umgekehrte Reihenfolge. All das wird von Sequenced Collections adressiert.

Einführende Beispiele

Bis Java 21 war es mühsam, auf das letzte Element einer Collection zuzugreifen. Schlimmer noch – es existieren diverse, jeweils vom Typ der Collection abhängige unterschiedliche Wege:

var lastListElement = list.get(list.size() - 1);
var lastSortedElement = sortedSet.last();
var lastDequeElement = deque.getLast();

Fast ebenso unhandlich waren mitunter Zugriffe auf das erste Element, insbesondere für Sets:

var firstElement = list.get(0);
var firstUnordered = hashSet.iterator().next();
var firstOrdered = treeSet.iterator().next();
var firstLhs = linkedHashSet.iterator().next();

Sich all diese Besonderheiten zu merken, ist mühsam und fehleranfällig. Selbst mit IDE-Unterstützung bleibt noch die mangelnde Eleganz. Schauen wir uns nun Sequenced Collections an und wie diese nicht nur diese Aufgabenstellung, sondern auch das Anfügen und Löschen von Elementen vereinfachen.

Abhilfe: Sequenced Collections

Bislang bietet Java keinen Typ, der eine geordnete Folge von Elementen repräsentiert. Wie angedeutet, füllt Java 21 diese Lücke durch die Sequenced Collections, genauer die Einführung der Interfaces SequencedCollection, SequencedSet und SequencedMap. Sie bieten Methoden zum Hinzufügen, Ändern oder Löschen von Elementen am Anfang oder Ende der Collection sowie zur Bereitstellung einer Collection in umgekehrter Reihenfolge.

Das Interface SequencedCollection

Betrachten wir das Interface SequencedCollection (Listing 9).

interface SequencedCollection<E> extends Collection<E> {
  SequencedCollection<E> reversed();
  void addFirst(E);
  void addLast(E);
  E getFirst();
  E getLast();
  E removeFirst();
  E removeLast();
}

Es ist leicht ersichtlich, dass SequencedCollection das Interface Collection erweitert und Methoden zum Hinzufügen, Ändern oder Löschen von Elementen am Anfang oder Ende bietet. Die Methoden addXyz() und removeXyz() lösen für Immutable Collections eine UnsupportedOperationException aus. Die Methode reversed() ermöglicht die Verarbeitung der Elemente in umgekehrter Reihenfolge. Tatsächlich handelt es sich dabei um eine View, ähnlich wie bei subList(). Dadurch werden Änderungen in der View auch in der Original-Collection sichtbar und vice versa.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Ein kurzer Blick hinter die Kulissen zeigt, wie es mit Hilfe von DefaultMethoden möglich wurde, die Interfaces in die bestehende Interfacehierarchie einzupassen (Listing 10).

public interface SequencedCollection<E> extends Collection<E> {
  SequencedCollection<E> reversed();

  default void addFirst(E e) {
    throw new UnsupportedOperationException();
  }

  default void addLast(E e) {
    throw new UnsupportedOperationException();
    }

  default E getFirst() {
    return this.iterator().next();
  }

  default E getLast() {
    return this.reversed().iterator().next();
  }

  default E removeFirst() {
    var it = this.iterator();
    E e = it.next();
    it.remove();
    return e;
  }

  default E removeLast() {
    var it = this.reversed().iterator();
    E e = it.next();
    it.remove();
    return e;
  }
}

Mit dieser Umsetzung sehen wir die Interfacehierarchie und die drei grün markierten neuen Interfaces der Sequenced Collections in Abbildung 2.

Abb. 2: Die Interfacehierarchie (neue Interfaces sind grün markiert) (Bildquelle: [1])

Die Interfaces SequencedSet und SequencedMap

Betrachten wir nun noch die beiden Interfaces SequencedSet und SequencedMap. Beginnen wir mit SequencedSet. Es erweitert Set und basiert auch auf SequencedCollection, allerdings ohne neue Methoden zu definieren und mit einer kleinen Abweichung bei reversed(), das eine kovariante Überschreibung mit geändertem Rückgabewert ist:

interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
  SequencedSet<E> reversed(); // covariant override
}

Analog zu SequencedCollection bietet SequencedMap die folgenden Methoden:

  • Entry<K, V> firstEntry() – liefert das erste Schlüssel-Wert-Paar
  • Entry<K, V> lastEntry() – liefert das letzte Schlüssel-Wert-Paar
  • Entry<K, V> pollFirstEntry() – entfernt das erste Schlüssel-Wert-Paar und gibt es zurück
  • Entry<K, V> pollLastEntry() – entfernt das letzte Schlüssel-Wert-Paar und gibt es zurück
  • V putFirst(K, V) – fügt ein Schlüssel-Wert-Paar am Anfang ein
  • V putLast(K, V) – fügt ein Schlüssel-Wert-Paar am Ende an
  • SequencedMap<K, V> reversed() – gibt eine View in umgekehrter Reihenfolge zurück

Damit ergibt sich die Interfacedefinition aus Listing 11.

interface SequencedMap<K,V> extends Map<K,V> {
  SequencedMap<K,V> reversed();
  SequencedSet<K> sequencedKeySet();
  SequencedCollection<V> sequencedValues();
  SequencedSet<Entry<K,V>> sequencedEntrySet();
  V putFirst(K, V);
  V putLast(K, V);
  Entry<K, V> firstEntry();
  Entry<K, V> lastEntry();
  Entry<K, V> pollFirstEntry();
  Entry<K, V> pollLastEntry();
}

SequencedMap-API

Das API von SequencedMap fügt sich nicht so gut in die Sequenced Collections ein. Es verwendet NavigableMap als Basis, daher bietet es statt getFirstEntry() die Methode firstEntry() und statt removeLastEntry() bietet es pollLastEntry(). Diese Namen korrespondieren nicht mit denen der SequencedCollection. Allerdings hätte der Versuch, dies zu tun, dazu geführt, dass NavigableMap vier neue Methoden erhalten hätte, die das Gleiche tun, wie die vier anderen Methoden, über die es bereits verfügt.

Sequenced Collections in Aktion

Zum Abschluss wollen wir die neuen Möglichkeiten einmal in Aktion erleben. Dazu definieren wir eine Liste mit Buchstaben und fragen im Anschluss das erste und das letzte Element ab. Danach erzeugen wir mit reversed() eine Collection mit umgekehrter Reihenfolge, die wir durchlaufen, zur Demonstration in einen Stream wandeln und die drei Elemente überspringen sowie schließlich das erste und letzte Element der umgekehrten Reihenfolge abfragen (Listing 12). Die Ausgabe zeigt Listing 13.

public static void sequenceCollectionExample() {
    System.out.println("Processing letterSequence with list");
    SequencedCollection<String> letterSequence = List.of("A", "B", "C", 
                                                         "D", "E");
    System.out.println(letterSequence.getFirst() + " / " +
                       letterSequence.getLast());

    System.out.println("Processing letterSequence in reverse order");
    SequencedCollection<String> reversed = letterSequence.reversed();
    reversed.forEach(System.out::print);
    System.out.println();
    System.out.println("reverse order stream skip 3");
    reversed.stream().skip(3).forEach(System.out::print);
    System.out.println();
    System.out.println(reversed.getFirst() + " / " + 
                       reversed.getLast());
    System.out.println();
}
Processing letterSequence with list
A / E
Processing letterSequence in reverse order
EDCBA
reverse order stream skip 3
BA
E / A

 

Variieren wir nun die Datenstruktur und nutzen Sets zur Verwaltung der Elemente. Zunächst einmal wird durch mehrmaliges Ausführen deutlich, dass die mit Set.of() erzeugten Daten keine fixe Reihenfolge besitzen, sondern dass diese von Ausführung zu Ausführung variiert. Am Beispiel eines TreeSet schauen wir uns die Möglichkeiten der Sequenced Collections, genauer des SequencedSet, an (Listing 14). Die Ausgabe zeigt Listing 15.

public static void sequencedSetExample() {
    // plain Sets do not have encounter order ... 
    // run multiple times to see variation
    System.out.println("Processing set of letters A-D");
    Set.of("A", "B", "C", "D").forEach(System.out::print);
    System.out.println();
    System.out.println("Processing set of letters A-I");
    Set.of("A", "B", "C", "D", "E", "F", "G", "H", "I").
        forEach(System.out::print);
    System.out.println();

    // TreeSet has order
    System.out.println("Processing letterSequence with tree set");
    SequencedSet<String> sortedLetters = 
                         new TreeSet<>((Set.of("C", "B", "A", "D")));
    System.out.println(sortedLetters.getFirst() + " / " + 
                       sortedLetters.getLast());
    sortedLetters.reversed().forEach(System.out::print);
    System.out.println();
}
Processing set of letters A-D
DCBA
Processing set of letters A-I
IHGFEDCBA
Processing letterSequence with tree set
A / D
DCBA

Fazit

Wir haben uns exemplarisch mit den Sequenced Collections als finalem Feature und mit den String Templates als vielversprechendem Preview Feature beschäftigt.

Sequenced Collections bereichern die ohnehin schon ausgefeilte Funktionalität des Collections-Frameworks und vereinheitlichen vor allem Zugriffe und Veränderungen des ersten und des letzten Elements sowie das Bereitstellen einer umgekehrten Reihenfolge.

Für die Konkatenation von fixen und variablen Textbestandteilen zu einem String gab es bisher schon diverse Varianten, alle mit spezifischen Stärken und Schwächen. String Templates werden die Art und Weise, wie man diese Aufgabe bewältigt, revolutionieren und stellen ein Feature dar, das Java wieder ein wenig moderner macht und zu anderen Sprachen wie Python oder Kotlin aufschließen lässt.

Java 21 bringt viele weitere Neuerungen, die die Programmiersprache voranbringen und attraktiver machen. Auch für komplette Newbies wird die Einstiegshürde gesenkt: Dank Unnamed Classes und Instance Main Methods lassen sich kleinere Java-Programme viel schneller und mit deutlich weniger Zeilen und für Anfänger schwierigen Begrifflichkeiten erstellen.

In diesem Sinne: Happy Coding mit dem brandaktuellen Java 21!


Links & Literatur

[1] https://cr.openjdk.org/~smarks/collections/SequencedCollectionDiagram20220216.png

Alle News der Java-Welt:

Behind the Tracks

Agile, People & Culture
Teamwork & Methoden

Clouds & Kubernetes
Alles rund um Cloud

Core Java & Languages
Ausblicke & Best Practices

Data & Machine Learning
Speicherung, Processing & mehr

DevOps & CI/CD
Deployment, Docker & mehr

Microservices
Strukturen & Frameworks

Performance & Security
Sichere Webanwendungen

Serverside Java
Spring, JDK & mehr

Software-Architektur
Best Practices

Web & JavaScript
JS & Webtechnologien

Digital Transformation & Innovation
Technologien & Vorgehensweisen

Domain-driven Design
Grundlagen und Ausblick

Spring Ecosystem
Wissen in Spring-Technologien

Web-APIs
API-Technologie, Design und Management