Das Wissensportal für IT-Professionals. Entdecke die Tiefe und Breite unseres IT-Contents in exklusiven Themenchannels und Magazinmarken.

SIGS DATACOM GmbH

Lindlaustraße 2c, 53842 Troisdorf

Tel: +49 (0)2241/2341-100

kundenservice@sigs-datacom.de

Pattern Matching in Java

Funktionale Programmierung (FP) wird als ein schlanker und eleganter Weg angesehen, Software zu implementieren. Pattern Matching ist ein integraler Baustein von FP, mit dem die Programmlogik definiert wird. Die Einführung von Pattern Matching in Java ist eines der Ziele von Projekt Amber.
Author Image
Merlin Bögershausen

Senior Software Engineer


  • 29.11.2023
  • Lesezeit: 10 Minuten
  • 60 Views

Bei Pattern Matching ermöglichen die zugrunde liegenden Summen- und Produktdatentypen Aussagen über die möglichen Ausprägungen und den Aufbau von Objekten. Projekt Amber hat mit Sealed Classes und Records Summen- beziehungsweise Produktdatentypen in Java eingeführt. Somit wurde der Grundstein für Pattern Matching in Java gelegt.

In diesem Artikel wird eine Bestandsaufnahme des Themenfelds Pattern Matching in Java 21 durchgeführt. Zur Veranschaulichung der Preview- und der finalisierten Features wird ein GitHub-Projekt des Autors kurz eingeführt und verwendet. Abschließend wird anhand der aktuellen Diskussionen in den Mailinglisten ein Blick in die Zukunft gewagt.

Beispiel Mehrwertsteuerrechner

Bei dem verwendeten Beispiel handelt es sich um eine naive Umsetzung eines Mehrwertsteuerrechners im Kontext einer Buchhaltungssoftware, die vereinfachte Domain ist in Abbildung 1 dargestellt.

Abb. 1: Stark vereinfachte Domain mit zwei Arten von Rechnungen und Kunden

In dem Rechner sind zwei Arten von Rechnungen modelliert, die sich vor allem durch ihre Zustellungsart unterscheiden und für diesen Artikel nicht relevant sind. Zusätzlich sind zwei Arten von Kunden modelliert, die stellenweise unterschiedlich zu behandeln sind. Grundsätzlich zahlen beide Kunden 18 Prozent Mehrwertsteuer, mit Ausnahme von Businesskunden, die eine Berechtigung zum Vorsteuerabzug besitzen.

In Listing 1 ist die Umsetzung der Domain mit Sealed Interfaces und Record Classes demonstriert.

sealed interface Kunde permits Privatkunde, Businesskunde {/*..*/}
record Privatkunde(String name, String mail) implements Kunde {/*..*/}
record Businesskunde(String name, String mail,
  boolean isVorsteuerAbzugsberechtigt) implements Kunde {/*..*/}
Listing 1: Umsetzung der Domain mit Sealed Interfaces und Record Classes

Diese Umsetzung ermöglicht es im Folgenden, alle Features von Pattern Matching in Java zu demonstrieren. Der Vollständigkeit halber seien in Listing 2 die im Folgenden behandelten Methoden calcualteMwSt und displayName gegeben.

double calculateMwSt(Kunde kunde, double wert) {
  if (kunde instanceof Privatkunde) return wert * 0.18d;
  if (kunde instanceof Businesskunde)
    return ((Businesskunde)kunde).isVorsteuerAbzugsberechtigt() ?
    0d : wert * 0.18d;
  /*..*/
}
String displayName(Kunde kunde) {
  if (kunde instanceof Privatkunde) return "P_" + kunde.name();
  if (kunde instanceof Businesskunde) return "B_" + kunde.name();
  /*..*/
}
Listing 2: Methoden calculateMwSt und displayName implementiert mit Java 11

Erstere bildet den Kern der Mehrwertsteuerberechnung auf Basis des Kunden und des Rechnungswerts, letztere sei relevant für eine Oberflächendarstellung.

An dieser Stelle ist es wichtig zu erwähnen, dass es sich um ein hypothetisches Szenario handelt und mit der fachlichen Realität wenig bis nichts zu tun hat. Der ständig aktualisierte und lauffähige Sourcecode ist auf GitHub [BoegDOP] frei verfügbar.

JEP 394 – Pattern Matching for instanceof

In Java ist es möglich, mit dem instanceof-Operator eine Fallunterscheidung auf Basis seines konkreten Datentyps einer Variablen durchzuführen. Fast immer wird dieser Operator mit einem anschließenden Typecast verbunden, um auf eine Methode oder ein Feld zuzugreifen. So auch bei der Behandlung des Businesskunden in den Zeilen 3 und 4 von Listing 2.

Hier wird geprüft, ob kunde ein Objekt vom Typ Businesskunde ist, zu diesem Typ gecastet, und die Methode isVorsteuerAbzugsberechtigt wird aufgerufen. Bei solchen Ausdrücken passiert es schnell, dass bei einem kleinen Refactoring eine Anpassung des Casts vergessen wird. Dies führt zu einer unangenehmen ClassCastException zum Ausführungszeitpunkt. In Listing 3 ist die Behandlung mit dem InstanceOf-Pattern realisiert.

if (kunde instanceof Businesskunde p)
  return p.isVorsteuerAbzugsberechtigt() ? 0d : wert * 0.18d;
Listing 3: Anwendung des instanceof-Pattern in der calculateMwSt-Methode

Diese in JEP 394 eingeführte kompaktere Schreibweise hat neben den offensichtlichen auch noch einige subtilere, aber umso schwergewichtigere Effekte. Syntaktisch fällt auf, dass der Zieldatentyp nicht mehr doppelt angegeben werden muss.

Der Cast wird automatisch durchgeführt und die neue Variable ist im positiven beziehungsweise im "matcht"-Zweig der Anweisung definiert. Zur Kompilierzeit können nun fehlerhafte Casts erkannt und als Kompilierfehler gemeldet werden. Dadurch wird der Ausdruck semantisch einfacher zu verstehen und zu refaktorieren.

Komplex wird es, wenn das Pattern noch logisch mit &&, || oder ! verknüpft wird. In diesen Fällen muss immer überlegt werden, "Welcher Zweig matcht?", denn nur in diesem Zweig sind die Variablen aus dem Pattern definiert und können verwendet werden.

Listing 4 ist nicht kompilierungsfähig.

if (!(kunde instanceof Businesskunde p))
  return p.isVorsteuerAbzugsberechtigt() ? 0d : wert * 0.18d;
Listing 4: Nicht kompilierungsfähig, da p nicht definiert

Durch die Negation des Patterns "matcht" das Pattern nur für den else-Zweig, also ist p nicht definiert. Eine noch größere Wirkung entfaltet dieses Pattern in Verbindung mit der switch-Anweisung beziehungsweise dem switch-Ausdruck.

JEP 427 – Pattern Matching for switch

Seit Java 14 und JEP 361 Switch Expressions kann switch auch als Expression, also einen Wert liefernd verwendet werden. Mit JEP 441 Pattern Matching for switch wird der switch-Ausdruck noch mächtiger, werden Patterns als Case-Labels für switch ermöglicht und in Java 21 finalisiert. In Listing 5 wurde das if-Statement durch eine switch-Expression mit Pattern ersetzt.

switch (kunde) {
  case Businesskunde b -> {
    if (b.isVorsteuerAbzugsberechtigt()) yield 0.0d;
    else yield wert * 0.1d;
 }
 case Privatkunde p -> wert * 0.1d;
};
Listing 5: Berechnung in calculateMwSt mit Switch-Expression und instanceof-Pattern

In dieser switch-Expression wird der Datentyp des Objekts kunde erneut mithilfe des Typ-Patterns aus JEP 394 geprüft. Im Beispiel oben wird unterschieden, ob es sich um einen Businessoder Geschäftskunden handelt. Da alle möglichen Ausprägungen von kunde explizit behandelt werden, ist an dieser Stelle ein Default nicht notwendig, der Compiler unterstützt an diesen Stellen mit verständlichen Fehlermeldungen.

Syntaktisch werden die Typ-Patterns als Case-Labels in einem switch verwendet und können, wie in Listing 6, mit sogenannten Guards zu Guarded Patterns kombiniert werden.

switch (kunde) {
  case Businesskunde b when b.isVorsteuerAbzugsberechtigt() -> 0.0d;
  case Businesskunde b -> wert * 0.1d;
  case Privatkunde p -> wert * 0.1d;
};
Listing 6: Berechnung in calculateMwSt mit Guarded Pattern

Guarded Patterns bestehen aus einem beliebigen Pattern und einem Booleschen Ausdruck (Guard), getrennt durch ein when. Der Zweig wird nur dann ausgewählt, wenn das Pattern "matcht" und der Guard zu true evaluiert wird. Im obigen Beispiel werden mittels eines Guarded Pattern die beiden möglichen Ausprägungen hinsichtlich des Vorsteuerabzuges bei Businesskunden unterschieden.

Doch was passiert, wenn die Zweige für den Businesskunden vertauscht werden? Um dies zu lösen, wurde die Semantik der Pattern Dominance eingeführt. Ein Pattern a dominiert ein anders Pattern b, wenn alle Objekte, die b "matcht“, auch von a "gematcht" werden, aber nicht umgekehrt. Für das Beispiel gilt:

  • Businesskunde b when b.isVorsteuerAbzugsberechtigt() „matcht“ nur vorsteuerabzugsberechtigt Businesskunden.
  • Businesskunde b „matcht“ alle Businesskunden.

Also der zweite Zweig dominiert den ersten. Sollten die Zweige vertauscht werden, wird das Guarded Pattern als unerreichbarer Code erkannt und führt zu einem Kompilierfehler. Eine umfassende Abhandlung zu dem Thema "Dominance of pattern labels" findet sich in der JEP 427 im Punkt 1b. Doch Pattern Matching besteht aus mehr als eleganten Typchecks.

JEP 440 – Record Patterns

Neben Prüfungen auf Datentypen gehört auch die Dekonstruktion von Objekten zum Umfang des Pattern Matching. Mit JEP 440 wird mit Java 21 das erste Dekonstruktions-Pattern in Java eingeführt und finalisiert. Als erstes Pattern wurde das Record Pattern ausgewählt, das ursprünglich ebenfalls im Titel enthaltene Array Pattern wurde auf eine zukünftige Version verschoben. Dekonstruktion ist die komplementäre Operation zum Konstruktor, darum wundert es auch nicht, dass die Syntax sehr ähnlich ist. In diesem und folgenden Absätzen wird die Methode displayName betrachtet.

In Listing 7 werden die Kunden in ihre Komponenten zerlegt und der Name mit einem Präfix versehen.

switch (kunde) {
  case Businesskunde(String name, String mail) -> "B_" + name;
  case Privatkunde(String name, String mail) -> "P_" + name;
};
Listing 7: Kunden in Komponenten zerlegen mit Record Pattern

Die neuen Variablen sind nun im "matcht"-Zweig verfügbar, eine Referenz auf den Punkt ist nicht verfügbar. Records können weitere Records enthalten und es ist auch möglich, Record Patterns beliebig zu verschachteln. Bei sogenannten Nested Patterns kann die Angabe der Datentypen, besonders bei Generics schnell zu einem sehr unübersichtlichen Code führen. Deswegen wurde in der zweiten Iteration dieses Features die Local Variable Type Inference wiederverwendet und in Listing 8 angewandt.

switch (kunde) {
  case Businesskunde(var name, var mail) -> "B_" + name;
  case Privatkunde(var name, var mail) -> "P_" + name;
};
Listing 8: Local Variable Type Inference in Record Patterns

Um den Datentyp von Komponenten automatisch bestimmen zu lassen, wird das Schlüsselwort var anstelle des Datentyps verwendet. Wie seit Java 10 gewohnt, veranlasst dieses Schlüsselwort den Compiler dazu, den Datentyp zur Compilezeit zu bestimmen. In diesem Beispiel ist der Gewinn zugegeben vernachlässigbar, jedoch sollte es einfach sein, eine passendere Situation zu erkennen.

JEP 443 – Unnamed Patterns and Variables

Project Amber versucht immer, den Pfad vom Einfachen zum Umfassenden zu wählen. Denn es ist immer erste Priorität, ein Feature zugeschnitten für die Allgemeinheit umzusetzen und später Spezialfälle abzudecken. Doch dadurch sind sowohl in displayNamen als auch in calculateMwSt viele ungenutzte Variablen zu finden. Mit JEP 443 werden genau diese Variablen adressiert. Vorbereitet wurde dies bereits in Java 9, als der Unterstrich "_" ein geschütztes Zeichen in Java wurde. Dieses Zeichen findet nun Anwendung in JEP 443, mit diesem ist es möglich, Variablen oder ganze Patterns zu ignorieren.

In Listing 9 wurde JEP 443 angewandt, um in den Methoden calculateMwSt und displayName die nicht relevanten Teile zu eliminieren.

// calculateMwSt
switch (kunde) {
  case Businesskunde b when b.isVorsteuerAbzugsberechtigt() -> 0.0d;
  case Businesskunde _, Privatkunde _-> wert * 0.1d;
};
// displayName
switch (kunde) {
  case Businesskunde(var name, _) -> "B_" + name;
  case Privatkunde(var name, _) -> "P_" + name;
};
Listing 9: Unnamed Variables in Record Patterns

Die Variablendefinitionen von Privat- und Businesskunde ohne Vorsteuerabzugsberechtigung wurden in der 4. Zeile durch ein unnamed pattern variables ersetzt. Hierdurch wird keine Variable für die gecasteten Kunden angelegt. Da es nun keine Variablen gibt, können die beiden Zweige zu einem verschmolzen werden.

Um an den Namen zu gelangen, musste in der Dekonstruktion des Kunden in der Methode displayName ebenfalls die Mailadresse entpackt werden. Durch den Einsatz von unnamed patterns konnte in der 9. und 10. Zeile die Definition var mail entfallen.

Eine weitere Anwendung der JEP 443 kann das Fangen und explizite Ignorieren von Exceptions sein. Wie in Listing 10 demonstriert, kann eine unnamed-Variable in Java auch als standardisiertes Sprachkonstrukt (zum Anzeigen von Desinteresse) verwendet werden. In diesem Fall wird klar, die eigentliche Exception ist nicht relevant, aber beachtet.

Zusammenfassung

In diesem Artikel wurde gezeigt, wie mächtig Pattern Matching bereits mit Java 21 ist. Durch die kompakte Schreibweise, die hohe Ausdrucksstärke und starke Unterstützung durch den Compiler ist es sehr einfach, hochqualitativen Code zu schreiben. Die nächsten Schritte werden die Behandlung von primitiven Datentypen und die Dekonstruktion von normalen Klassen sein, so zumindest der entstehende Eindruck beim Betrachten der JEP Drafts [JEPDRAFT] und Amber-Projekt-Seite [AMBER].

Weitere Informationen

[AMBER] https://openjdk.org/projects/amber/

[BoegDOP] DataOrientedJava,
https://github.com/MBoegers/DataOrientedJava

[JEP361] Switch Expressions,
https://openjdk.org/jeps/361

[JEP394] Pattern Matching for instanceof,
https://openjdk.org/jeps/394

[JEP427] Pattern Matching for switch (Third Preview),
https://openjdk.org/jeps/427

[JEP440] Record Patterns,
https://openjdk.org/jeps/440

[JEP441] Pattern Matching for switch,
https://openjdk.org/jeps/441

[JEP443] Unnamed Patterns and Variables (Preview),
https://openjdk.org/jeps/443

[JEPDRAFT] JDK Enhancement Proposals, Index,
https://openjdk.org/jeps/0

. . .

Author Image

Merlin Bögershausen

Senior Software Engineer
Zu Inhalten

Merlin Boergershausen ist Senior Software Engineer bei der adesso SE mit über 10 Jahren Erfahrung in Enterprise Java. Er beschäftigt sich in Artikeln und Talks schwerpunktmäßig mit Neuerungen in der Sprache Java, mit Ausflügen in unterschiedlichste Nischen der Softwareentwicklung. Er freut sich immer über eine Diskussion um das Für und Wider von Neuerungen, denn nur in der Diskussion kann sich ein guter Best Practice bilden.


Artikel teilen