In diesem Artikel erfährst du:
- Was ist Pattern Matching?
- Wie können wir primitive Typen im Pattern Matching mit instanceof verwenden?
- Wie können wir primitive Typen im Pattern Matching mit switch verwenden?
- Was ist der Unterschied zwischen Pattern Matching mit primitiven Typen und mit Objekt-Typen („Referenztypen“)?
- Was sind dominierende und dominierte primitive Typen?
Wir beginnen mit einem kurzen Auffrischer über Pattern Matching in Java. Wenn du bereits mit Pattern Matching im Allgemeinen vertraut bist, überspringe gerne das Einführungskapitel und gehe direkt zum zweiten Kapitel, Neuerungen in Java 23.
Was ist Pattern Matching?
Pattern Matching in Java wurde erstmals in Java 16 mit Pattern Matching for instanceof als finales Feature veröffentlicht und in Java 21 um Pattern Matching for switch erweitert.
Das folgende Code-Beispiel benutzt Pattern Matching, um herauszufinden, ob die Variable obj
vom Typ String ist, und wenn ja, diesen in Großbuchstaben konvertiert auszugeben:
Object obj = . . .
if (obj instanceof String s) {
System.out.println(s.toUpperCase());
}
Code-Sprache: Java (java)
Das Pattern ist in diesem Beispiel String s
. Der Code prüft zunächst, ob die Object
-Variable obj
auf dieses Pattern „matcht“. Das tut sie, wenn obj
vom Typ String
ist. Wenn das der Fall ist, wird der Inhalt von obj
in der String-Variablen s
zur Verfügung gestellt, in Großbuchstaben konvertiert und ausgegeben.
Das folgende Beispiel ist etwas komplexer und benutzt switch
statt instanceof
, um die Variable obj
mit verschiedenen Patterns abzugleichen und um je nach Typ verschiedene Aktionen durchzuführen:
switch (obj) {
case String s when s.length() >= 5 -> System.out.println(s.toUpperCase());
case Integer i -> System.out.println(i * i);
case Number n -> System.out.println(n + " is a number");
case null, default -> System.out.println(obj);
}
Code-Sprache: Java (java)
Das erste Pattern, String s when s.length() >= 5
, ist ein sogenanntes „Guarded Pattern“, ein Pattern mit einer Einschränkung, und s.length() >= 5
ist der „Guard“. Die Variable obj
matcht dieses Pattern dann, wenn sie vom Typ String ist und dieser String mindestens fünf Zeichen lang ist.
Das zweite Pattern, Integer i
, matcht, wenn obj
vom Typ Integer
ist.
Das dritte Pattern, Number n
, matcht, wenn obj
von einem von der abstrakten Klasse Number
abgeleiteten Typ ist, also z. B. Long
oder Double
, aber auch AtomicInteger
oder BigDecimal
. Das Pattern würde auch auf Variablen vom Typ Integer
matchen, aber die werden bereits in der Zeile zuvor durch das Pattern Integer i
„abgefangen“.
Neuerungen in Java 23
Bisher funktioniert Pattern Matching nur mit Referenztypen, also z. B. String
und Integer
, nicht aber mit primitiven Typen wie int
, long
und double
.
In Java 23 wurde durch JDK Enhancement Proposal 455 das Preview-Feature „Primitive Types in Patterns, instanceof, and switch“ vorgestellt, in Java 24 wird das Feature durch JEP 488 ohne Änderungen wiedervorgelegt.
Wenn du dieses Feature mit --enable-preview
aktivierst, kannst du:
- primitive Typen im Pattern Matching verwenden,
- in
switch
Konstanten der Typenlong
,float
,double
undboolean
verwenden.
Die erste Änderung beschreibe ich im Detail im kommenden Abschnitt, Primitive Typen im Pattern Matching. Die zweite Änderung erkläre ich schnell an dieser Stelle:
Seit jeher können wir mit switch
eine Variable mit Konstanten vergleichen, z. B. wie folgt:
int code = . . .
switch (code) {
case 200 -> System.out.println("OK");
case 400 -> System.out.println("Bad Request");
case 404 -> System.out.println("Not Found");
. . .
}
Code-Sprache: Java (java)
Das funktioniert allerdings bisher nur mit den Typen byte
, short
, char
und int
. Wenn du in der ersten Zeile int
beispielsweise durch long
ersetzt, bekommst du einen Compilerfehler:
error: selector type long is not allowed
Code-Sprache: Klartext (plaintext)
Wenn du in Java 23 oder 24 das Feature „Primitive Types in Patterns, instanceof, and switch“ mit --enable-preview
aktivierst, verschwindet diese Fehlermeldung. Du darfst dann jeden beliebigen primitiven Typ in switch
verwenden.
Primitive Typen im Pattern Matching
Ein Objekt matcht ein Pattern, wenn das Objekt einer Variable vom Typen des Pattern zugewiesen kann. Wie du im vorherigen Abschnitt gesehen hast, matcht z. B. ein Integer
-Objekt auf das Pattern Integer i
– es würde aber auch auf die Pattern Number n
oder Object o
– und sogar auf Comparable c
oder Serializable s
matchen, denn Integer
erbt von Number
und implementiert u. a. Comparable
, und Number
erbt von Object
und implementiert Serializable
:
Bei primitiven Typen gibt es allerdings keine Vererbung. Daher funktioniert Pattern Matching bei primitiven Typen nicht genau wie bei Referenztypen – aber ähnlich.
Im folgenden Abschnitt erkläre ich dir zunächst, wie primitive Typen im Pattern Matching mit instanceof verwendet werden. Im darauf folgenden Abschnitt zeige ich dir dann primitive Typen im Pattern Matching mit switch.
Primitive Typ-Pattern mit instanceof
Nicht erschrecken: ich beginne mit einer mathematisch klingenden Formulierung, erkläre dann aber sofort an einem Beispiel, was gemeint ist.
Sei a
eine Variable eines primitiven Typen (also byte
, short
, int
, long
, float
, double
, char
oder boolean
) und B
einer eben dieser primitiven Typen. Dann ergibt a instanceof B
genau dann true
, wenn der präzise Wert von a
auch in einer Variablen vom Typ B
gespeichert werden kann.
Beispiel 1
Hier kommt schon das Beispiel:
int value = . . .
if (value instanceof byte b) {
System.out.println("b = " + b);
}
Code-Sprache: Java (java)
Der Code ist wie folgt zu lesen: Wenn der Wert der Variablen value
auch in einer byte
-Variablen gespeichert werden kann, dann weise der byte
-Variablen b
diesen Wert zu und gebe ihn aus.
Für value = 5
wäre das z. B. der Fall, für value = 1000
hingegen nicht, da eine Variable vom Typ byte
lediglich Werte von -128 bis 127 speichern kann.
Beispiel 2
Hier ein zweites Beispiel:
double value = . . .
if (value instanceof float f) {
System.out.println("f = " + f);
}
Code-Sprache: Java (java)
Hier wird geprüft, ob das double
value
auch als float
dargestellt werden kann. Das wäre z. B. für value = 1.5
der Fall, für value = Math.PI
aber nicht, da float
nicht präzise genug ist, um alle Stellen der double
-Konstante Math.PI
aufzunehmen.
Beispiel 3
Weisen wir value
mal einen konkreten Wert zu und prüfen diesen gegen alle numerischen primitiven Typen (ein Vergleich numerischer Typen mit boolean
ist nicht erlaubt und führt zu einem Comilerfehler).
Hier ist, anstelle eines Code-Schnipsels, ein vollständiges, ausführbares Demo-Programm:
void main() {
int value = 65;
if (value instanceof byte b) System.out.println(value + " instanceof byte: " + b);
if (value instanceof short s) System.out.println(value + " instanceof short: " + s);
if (value instanceof int i) System.out.println(value + " instanceof int: " + i);
if (value instanceof long l) System.out.println(value + " instanceof long: " + l);
if (value instanceof float f) System.out.println(value + " instanceof float: " + f);
if (value instanceof double d) System.out.println(value + " instanceof double: " + d);
if (value instanceof char c) System.out.println(value + " instanceof char: " + c);
}
Code-Sprache: Java (java)
Wenn du das Programm z. B. in der Datei Test.java speicherst, dann kannst du es in Java 23 und 24 wie folgt starten:
java --enable-preview Test.java
Code-Sprache: Klartext (plaintext)
Du siehst dann folgende Ausgabe:
65 instanceof byte: 65
65 instanceof short: 65
65 instanceof int: 65
65 instanceof long: 65
65 instanceof float: 65.0
65 instanceof double: 65.0
65 instanceof char: A
Code-Sprache: Klartext (plaintext)
Der Wert 65 kann also in Variablen aller anderen primitiven Typen (außer boolean
) gespeichert werden. Du siehst hier sehr schön, dass dieser Wert als float
und double
mit einer Nachkommastelle dargestellt wird und als char
als das Zeichen 'A' (dessen ASCII-Code die 65 ist).
Beispiel 4
Wenn wir value
auf 100.000 ersetzen, kommt folgende Ausgabe heraus:
100000 instanceof int: 100000
100000 instanceof long: 100000
100000 instanceof float: 100000.0
100000 instanceof double: 100000.0
Code-Sprache: Klartext (plaintext)
Der Wert 100.000 kann also in Variablen vom Typ int
, long
, float
und double
gespeichert werden, nicht aber in Variablen vom Typ byte
, short
und char
. Deren Zahlenraum geht nur bis 127, 32.767 und 65.535.
Beispiel 5
Interessant wird es für value = 16_777_217
:
16777217 instanceof int: 16777217
16777217 instanceof long: 16777217
16777217 instanceof double: 1.6777217E7
Code-Sprache: Klartext (plaintext)
Die Zahl 16.777.217 kann also in int
, long
und double
gespeichert werden, nicht aber in float
?
Das ist tatsächlich der Fall! Lass einmal folgenden Code laufen:
float f = 16_777_217;
System.out.printf("f = %.1f%n", f);
Code-Sprache: Java (java)
Das Ergebnis ist unerwartet:
f = 16777216.0
Code-Sprache: Klartext (plaintext)
Die ausgegebene Zahl endet auf 6, nicht auf 7!
Das liegt daran, dass der Gleitkommatyp float
eine begrenzte Genauigkeit hat und zwar beispielsweise 16.777.216, 16.777.218 und 16.777.220 speichern kann, nicht aber die dazwischen liegenden Werte 16.777.217 und 16.777.219.
Beispiel 6
Im folgenden Beispiel ist value
eine Gleitkommazahl vom Typ float
:
void main() {
float value = 3.5f;
if (value instanceof byte b) System.out.println(value + " instanceof byte: " + b);
if (value instanceof short s) System.out.println(value + " instanceof short: " + s);
if (value instanceof int i) System.out.println(value + " instanceof int: " + i);
if (value instanceof long l) System.out.println(value + " instanceof long: " + l);
if (value instanceof float f) System.out.println(value + " instanceof float: " + f);
if (value instanceof double d) System.out.println(value + " instanceof double: " + d);
if (value instanceof char c) System.out.println(value + " instanceof char: " + c);
}
Code-Sprache: Java (java)
Jetzt gibt das Programm folgendes aus:
3.5 instanceof float: 3.5
3.5 instanceof double: 3.5
Code-Sprache: Klartext (plaintext)
Klar, denn eine Zahl mit Nachkommastellen ist natürlich nur mit float
und double
darstellbar.
Beispiel 7
Wenn wir value
aber auf 100000.0f
setzen, sieht das Ergebnis wie folgt aus:
100000.0 instanceof int: 100000
100000.0 instanceof long: 100000
100000.0 instanceof float: 100000.0
100000.0 instanceof double: 100000.0
Code-Sprache: Klartext (plaintext)
Die Gleitkommazahl 100.000,0 kann, da sie keine Nachkommastellen hat, auch in einem int
oder einem long
gespeichert werden.
Pattern Matching mit boolean
boolean
darf übrigens nur mit boolean
vergleichen werden. Jeder Vergleich von boolean
mit einem anderen Typ oder einem anderen Typ mit boolean
führt zu einem „incompatible types“ Compilerfehler.
Viel bringt uns Pattern Matching mit boolean
ohnehin nicht, denn ein Pattern-Abgleich einer booleschen Variablen mit dem Typ boolean
ergibt immer true
.
Primitive Typ-Pattern mit instanceof und &&
Genau wie bei Referenztypen darfst du auch bei primitiven Typen direkt im instanceof
-Check mit &&
weitere Prüfungen anschließen. Der folgende Code z. B. gibt nur positive byte
-Werte (also 1 bis 127) aus:
int a = . . .
if (a instanceof byte b && b > 0) {
System.out.println("b = " + b);
}
Code-Sprache: Java (java)
Primitive Typ-Pattern mit switch
Wir können primitive Pattern nicht nur in instanceof
einsetzen, sondern auch in switch
:
void main() {
double value = 100000.0;
switch (value) {
case byte b -> System.out.println(value + " instanceof byte: " + b);
case short s -> System.out.println(value + " instanceof short: " + s);
case char c -> System.out.println(value + " instanceof char: " + c);
case int i -> System.out.println(value + " instanceof int: " + i);
case long l -> System.out.println(value + " instanceof long: " + l);
case float f -> System.out.println(value + " instanceof float: " + f);
case double d -> System.out.println(value + " instanceof double: " + d);
}
}
Code-Sprache: Java (java)
Das Programm führt zu folgender Ausgabe:
100000.0 instanceof int: 100000
Code-Sprache: Klartext (plaintext)
Wir sehen hier nicht alle matchenden Pattern, sondern nur das erste, da durch switch
ja immer nur ein einziger Programmpfad ausgeführt wird.
Hier ein paar Beispiele für value
zusammen mit dem Typ des ersten matchenden Patterns:
value | Erster matchender Typ | Zahlenraum des matchenden Typs |
---|---|---|
0 | byte | -128 bis 127 |
10.000 | short | -32.768 bis 32.767 |
50.000 | char | 0 bis 65.535 |
1.000.000 | int | -2.147.483.648 bis 2.147.483.647 |
1.000.000.000.000 | long | ca. minus bis plus 9 Trillionen |
0.125 | float | Fließkommazahlen mit einfacher Genauigkeit |
0.126 | double | Fließkommazahlen mit doppelter Genauigkeit |
Primitive Typ-Pattern mit switch und when („Guarded Pattern“)
Auch bei primitiven Typ-Pattern in switch
können wir „guards” verwenden, also das Pattern mit when
mit einem booleschen Ausdruck versehen. Das kann z. B. dann hilfreich sein, wenn wir nach Zahlenbereichen gruppieren wollen, wie z. B. nach HTTP-Statuscodes.
Hier ein Beispiel, das bisher nur mit einer if
-else-Kette möglich war:
private String getHttpStatusMessage(int code) {
if (code == 200) return "OK";
else if (code == 400) return "Bad request";
else if (code == 404) return "Not found";
else if (code == 500) return "Internal server error";
else if (code > 100 && code < 200) return "Informational";
else if (code > 200 && code < 300) return "Success";
else if (code > 302 && code < 400) return "Redirection";
else if (code > 400 && code < 500) return "Client error";
else if (code > 502 && code < 600) return "Server error";
else return "Unknown code";
}
Code-Sprache: Java (java)
Diese Methode können wir in Zukunft – meiner Meinung nach deutlich übersichtlicher – mit einem switch
wie folgt schreiben:
private String getHttpStatusMessage(int code) {
return switch (code) {
case 200 -> "OK";
case 400 -> "Bad request";
case 404 -> "Not found";
case 500 -> "Internal server error";
case int i when i > 100 && i < 200 -> "Informational";
case int i when i > 200 && i < 300 -> "Success";
case int i when i > 302 && i < 400 -> "Redirection";
case int i when i > 400 && i < 500 -> "Client error";
case int i when i > 502 && i < 600 -> "Server error";
default -> "Unknown code";
};
}
Code-Sprache: Java (java)
Dominierende und dominierte primitive Typen
Bei switch
mit primitiven Typen müssen wir – genau wie bei Objekttypen – das Prinzip der dominierenden und dominierten Typen beachten.
Ein dominierender Typ ist einer, der alle Werte eines dominierten Typs darstellen kann.
Z. B. wird byte
von int
dominiert, da jedes byte
auch als int
dargestellt werden kann. Schau dir einmal den folgenden Code an.
double value = . . .
switch (value) {
case int _ -> System.out.println(value + " instanceof int"); // dominating type
case byte _ -> System.out.println(value + " instanceof byte"); // dominated type
case double _ -> System.out.println(value + " instanceof double");
}
Code-Sprache: Java (java)
Das case byte
-Label würde in diesem Fall niemals matchen, da jedes byte
auch ein int
ist und somit bereits vom case int
-Label abgefangen werden würde.
Wenn du versuchen würdest, diesen Code zu compilieren, würde das zu folgendem Compilerfehler führen:
error: this case label is dominated by a preceding case label
case byte _ -> System.out.println(value + " instanceof byte");
^
Code-Sprache: Klartext (plaintext)
Generell gilt: Ein dominierter Typ muss immer vor einem dominierenden Typ aufgeführt sein. Folgendes ist also OK:
double value = . . .
switch (value) {
case byte _ -> System.out.println(value + " instanceof byte"); // dominated type
case int _ -> System.out.println(value + " instanceof int"); // dominating type
case double _ -> System.out.println(value + " instanceof double");
}
Code-Sprache: Java (java)
Vollständigkeitsprüfung bei switch
Bei allen neuen (d. h. bei allen seit Java 21 hinzugekommenen) switch
-Features gilt: der switch
muss vollständig sein, es muss also für jeden möglichen Wert des Selektor-Ausdrucks (im Beispiel die Variable value
) ein matchendes case-Label existieren.
Deshalb enthielten die vorherigen Beispiele auch ein case double
-Label. Folgendes wäre nicht erlaubt:
double value = . . .
switch (value) {
case byte _ -> System.out.println(value + " instanceof byte");
case int _ -> System.out.println(value + " instanceof int");
}
Code-Sprache: Java (java)
Dieser switch
ist unvollständig und damit ungültig, da z. B. für den Wert 3,5 kein case
-Label matchen würde. Der Compiler würde dies mit folgendem Fehler quitieren:
error: the switch statement does not cover all possible input values
switch (value) {
^
Code-Sprache: Klartext (plaintext)
Folgender switch
ist hingegen vollständig:
short value = . . .
switch (value) {
case byte _ -> System.out.println(value + " instanceof byte");
case int _ -> System.out.println(value + " instanceof int");
}
Code-Sprache: Java (java)
Hier gibt es zwar kein case short
-Label, aber ein case int
-Label, und gegen das matcht jeder mögliche short
-Wert.
Zusammenfassung
Mit der Option --enable-preview
kannst du in Java 23 und 24 das Feature „Primitive Types in Patterns, instanceof, and switch“ aktivieren. Damit kannst du mit instanceof
und switch
gegen primitive Typ-Pattern wie z. B. int i
oder double d
matchen.
Da es bei primitiven Typen keine Vererbung gibt, funktionieren primitive Pattern etwas anders als Pattern mit Referenztypen: eine Variable matcht ein primitives Pattern dann, wenn eine Variable des Zieltyps sie ohne Präzisionsverlust aufnehmen kann.
Möchtest du immer auf dem neuesten Stand bleiben und informiert werden, sobald ein neuer Artikel auf HappyCoders.eu veröffentlicht werden? Dann klicke hier, um dich für den HappyCoders.eu-Newsletter anzumelden.