primitive types in patterns, instanceof, and switch - feature imageprimitive types in patterns, instanceof, and switch - feature image
HappyCoders Glasses

Primitive Typen in Patterns, instanceof und switch

Sven Woltmann
Sven Woltmann
Aktualisiert: 4. Dezember 2024

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:

  1. primitive Typen im Pattern Matching verwenden,
  2. in switch Konstanten der Typen long, float, double und boolean 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 allowedCode-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:

Klassendiagramm: Integer erweitert Number, Number erweitert Object

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.javaCode-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:   ACode-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.0Code-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.6777217E7Code-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.0Code-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.5Code-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.0Code-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:    100000Code-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:

valueErster matchender TypZahlenraum des matchenden Typs
0byte-128 bis 127
10.000short-32.768 bis 32.767
50.000char0 bis 65.535
1.000.000int-2.147.483.648 bis 2.147.483.647
1.000.000.000.000longca. minus bis plus 9 Trillionen
0.125floatFließkommazahlen mit einfacher Genauigkeit
0.126doubleFließ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.

In den folgenden Beispielen habe die in Java 22 finalisierte unbenannte Variable _ (den Unterstrich) verwendet.

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.