Java Pattern Matching
Pattern Matching in Java für instanceof und switch – Typmuster, Record-Muster und Destrukturierung erklärt.
Jahrelang folgte Java-Code, der mit Werten unbekannten Typs arbeitete, einem mühsamen Ritual: den Typ mit instanceof prüfen, dann zu diesem Typ casten und dann verwenden. Pattern Matching fasst dieses Ritual in einem einzigen Ausdruck zusammen. Ein Muster beschreibt die Form der Daten; wenn ein Wert übereinstimmt, bindet Java seine Teile an Variablen, die sofort verwendet werden können – kein manueller Cast erforderlich.
Pattern Matching kam schrittweise: zuerst instanceof-Muster, dann Muster in switch, dann Record-Muster, die Records in ihre Komponenten zerlegen. Zusammen ermöglichen sie das Schreiben von deklarativem, typsicherem Code, der wie die Daten aussieht, mit denen er arbeitet.
Dieses Kapitel behandelt das instanceof-Muster, Typmuster in switch, bewachte Muster und null-Behandlung sowie Record-Muster – und verbindet sie in einem ausführbaren Programm. Es baut auf drei Funktionen auf, die Sie vielleicht zuerst nachlesen möchten: dem instanceof-Operator, Records und switch-Ausdrücken.
Pattern Matching für instanceof
Das klassische Test-und-Cast-Muster benötigte drei Referenzen auf denselben Typ. Das instanceof-Muster bindet eine Variable im selben Zug wie der Test, und die Bindung gilt überall dort, wo der Test als wahr bekannt ist.
Object value = "hello";
// Old way: test, then cast
if (value instanceof String) {
String s = (String) value;
System.out.println(s.length());
}
// Pattern way: test and bind together
if (value instanceof String s) {
System.out.println(s.length());
}Da die Bindungsvariable am booleschen Ausdruck teilnimmt, können Sie in derselben if-Anweisung weiter einengen. Der Compiler beweist, dass s sicher zu verwenden ist:
if (value instanceof String s && s.length() > 3) {
System.out.println(s.toUpperCase());
}Muster in switch
Ein switch kann auf Typmuster abgleichen und nach dem Laufzeittyp des Selektors dispatchen. Jeder case bindet den abgeglichenen Wert, sodass der Rumpf direkt mit einer typisierten Variable arbeitet. Dies wandelt lange if/else instanceof-Ketten in eine kompakte, lesbare Tabelle um.
static String format(Object value) {
return switch (value) {
case Integer i -> "int: " + i;
case Long l -> "long: " + l;
case String s -> "string: " + s;
default -> "other: " + value;
};
}Ein typmusterbasierter switch muss erschöpfend sein – er muss jede mögliche Eingabe abdecken. Für beliebige Object-Selektoren bedeutet das einen default-Zweig; für sealed-Hierarchien kennt der Compiler die vollständige Menge der Untertypen und kann die Erschöpflichkeit ohne default prüfen.
Bewachte Muster und null
Eine when-Klausel fügt einem case eine boolesche Bedingung hinzu und ermöglicht es zwei Werten desselben Typs, verschiedene Zweige zu nehmen. Dies wird als bewachtes Muster bezeichnet, und die Reihenfolge ist wichtig: spezifischere bewachte Cases kommen vor dem unbewachten Fallback.
static String size(String s) {
return switch (s) {
case String t when t.isEmpty() -> "empty";
case String t when t.length() < 5 -> "short";
case String t -> "long (" + t.length() + ")";
};
}Traditionell warf ein switch bei einem null-Selektor eine NullPointerException. Ein musterbasierter switch kann null explizit mit einem case null behandeln und die Null-Prüfung innerhalb desselben Konstrukts behalten, anstatt sie als separate Prüfung davor zu schreiben.
| Funktion | Syntax | Zweck |
|---|---|---|
| Typmuster | case String s | Nach Typ abgleichen und binden |
| Bewachtes Muster | case String s when s.isEmpty() | Eine Bedingung zu einem Case hinzufügen |
| Null-Label | case null | Einen null-Selektor abgleichen |
| Record-Muster | case Point(int x, int y) | Einen Record destrukturieren |
Record-Muster
Ein Record-Muster gleicht einen Record ab und bindet seine Komponenten in einem Schritt, sodass die Accessor-Aufrufe entfallen. Da Records ihre Komponenten offenlegen, kennt der Compiler die genaue Form und lässt Sie jeden Teil inline benennen. Record-Muster können verschachtelt werden, sodass Sie einen Record von Records destrukturieren können.
record Point(int x, int y) {}
record Line(Point start, Point end) {}
static String render(Object o) {
return switch (o) {
case Point(int x, int y) -> "point " + x + "," + y;
// Nested: pull both endpoints' coordinates out at once
case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
"line " + x1 + "," + y1 + " -> " + x2 + "," + y2;
default -> "unknown";
};
}Pattern Matching glänzt besonders mit sealed-Typen: Wenn eine Schnittstelle ihre zulässigen Implementierungen auflistet, ist ein switch darüber ohne default erschöpfend, und das Hinzufügen eines neuen Untertyps macht den fehlenden Case zu einem Compilerfehler statt einem stillen Bug.
Ein vollständiges, ausführbares Beispiel
Das folgende Programm verbindet alle Teile. Es verwendet ein instanceof-Muster mit einem Guard, eine sealed Shape-Hierarchie aus Records, Record-Muster, die jede Form in einem switch destrukturieren, ein bewachtes Muster, das ein Quadrat erkennt, und ein case null – alles ohne einen einzigen expliziten Cast.
Was aus der Ausführung zu entnehmen ist:
describe(42)gibtpositive int 42aus, weil der Guardinstanceof Integer i && i > 0den Typ und den Wert zusammen prüft, bevorigebunden wird.describe(-5)fällt aufunknownzurück – dasselbeInteger-Muster stimmt mit dem Typ überein, aber der Guardi > 0schlägt fehl, was zeigt, wie ein Guard ein Typmuster verfeinert.- Der
area-Switch benötigt keindefault:Shapeist sealed, daher ist das Auflisten vonCircle,RectangleundTriangleerschöpfend, und der Compiler ist zufrieden. - Das
5.0 x 5.0-Rechteck wird alssquare side=5.0ausgegeben, weil sein bewachterwhen w == h-Case vor dem allgemeinenRectangle r-Case platziert ist und gewinnt. - Die letzte Zeile gibt
no shapeaus: Dercase null-Zweig behandelt einennull-Selektor innerhalb des Switch, anstatt eineNullPointerExceptionzu werfen.