Die vom Anbeginn der Sprache Java vorhandenen switch-Statements sind sehr eng an die switch-Statements aus C/C++ angelehnt und haben einige Probleme. Obwohl switch-Statements in der Praxis oft eingesetzt werden um auf unterschiedliche Weise Werte zu berechnen ist es bisher nicht möglich gewesen switch in einem Expression-Kontext zu verwenden.

Darüber hinaus fällt der Kontrollfluss durch case-Zweige hindurch wenn sie nicht durch ein break beendet werden. Dieses als Fall-Through bekannte Verhalten kann bewusst eingesetzt werden um für verschiedene Werte gegen die switch prüft dasselbe Verhalten auszuführen. Es besteht jedoch die Gefahr, daß durch vergessene break’s teilweise schwer sichtbare Fehler entstehen.
Manchmal ist auch der Scope relevant den switch aufspannt. Das switch-Statement an sich öffnet einen eigenen Scope, während einzelne case-Zweige dies nicht tun, was dazu führt, daß Variablen die in den case-Zweigen definiert werden im gesamten Kontext des switch gelten und in Konflikt zueinander stehen können.
Zu guter Letzt konnten switch-Statements bisher nicht mit null Werten umgehen, was ab Java 21 mittels eines case null Case-Zweiges adressiert werden kann (JEP-427 hat dies als Preview ab Java 19 bereitgestellt).

Mit Java 14 wurden die switch-Expressions final als Feature in die Sprache Java aufgenommen (https://openjdk.org/jeps/361) die die vorhandenen switch-Statements im Hinblick auf die o.g. Probleme weiter entwickeln.
Für Java 21 und mit “Pattern Matching for switch” (JEP-441) sowie “Record Patterns” (JEP-440) wurden dann weitere Verbesserungen für switch-Expressions als finales Feature in die Sprache aufgenommen die weiter unten auch kurz vorgestellt werden.

Switch-Expressions

Es ist nun möglich switch in einem Expression-Kontext zu verwenden und ohne “Fall Through” Verhalten zu verwenden. Hierfür ist die case <Label>(,<Label>)* -> Syntax zu verwenden, wie nachfolgende Beispiele (aus JEP-361) zeigen.

    // Hier findet KEIN "Fall through" statt. Die Expression-Eigenschaft wird hier aber nicht genutzt (Der Typ dieser Expression wäre 'Void')
    switch (day) {
        case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
        case TUESDAY                -> System.out.println(7);
        case THURSDAY, SATURDAY     -> System.out.println(8);
        case WEDNESDAY              -> System.out.println(9);
    }

    // In diesem Beispiel wird die Expression-Eigenschaft genutzt.
    int numLetters = switch (day) {
        case MONDAY, FRIDAY, SUNDAY -> 6;
        case TUESDAY                -> 7;
        case THURSDAY, SATURDAY     -> 8;
        case WEDNESDAY              -> 9;
    };

wie in den Beispielen ersichtlich ist es nun möglich, daß ein Case-Zweig mehrere Werte prüft, so daß es nicht mehr notwendig ist hier mit redundatem Code oder “Fall-Through” zu arbeiten. Folgender (alter) Code kann mit dem 2. Beispiel oben deutlich vereinfacht und durch Verzicht auf “Fall Through” sicherer gemacht werden.

    // Beispiel aus https://openjdk.org/jeps/361
    int numLetters;
    switch (day) {
        case MONDAY:
        case FRIDAY:
        case SUNDAY:
            numLetters = 6;
            break;
        case TUESDAY:
            numLetters = 7;
            break;
        case THURSDAY:
        case SATURDAY:
            numLetters = 8;
            break;
        case WEDNESDAY:
            numLetters = 9;
            break;
        default:
            throw new IllegalStateException("Wat: " + day);
    }

Vollständigkeitsprüfung

Bei der Verwendung von switch-Expressions muss für jeden möglichen Wert ein Case-Zweig “matchen”, da ansonsten die Expression keinen Wert hätte.

Aus diesem Grund führt der Compiler für switch-Expressions eine Vollständigkeitsprüfung durch. Für Enums kann die Vollständigkeit sichergestellt werden, indem alle möglichen Enum-Werte in Case-Zweigen berücksichtigt werden und für Sealed Classes (relevant ab Java 21 / “Pattern Matching for switch” s.u.) indem sämtliche möglichen Typen abgedeckt werden. Ist dies nicht gewollt, oder wird z.b. gegen int geprüft, so kann alternativ ein default-Zweig dafür sorgen, das die switch-Expression vollständig ist.

Case-Zweige mit Blocks auf der rechten Seite

Sind zur Berechnung des Wertes eines Case-Zweiges mehrere Statements notwendig, oder soll ein Seiteneffekt wie z.b. für Logging notwendig erziehlt werden, so kann auf der rechten Seite eines case L -> Zweiges statt einer Expression auch ein Block verwendet werden. In diesem Fall wird der Wert aus diesem Block mittels des Schlüsselwortes yield zurückgegeben. Das folgende aus JEP-361 stammende Beispiel verdeutlicht dies.

    int j = switch (day) {
        case MONDAY  -> 0;
        case TUESDAY -> 1;
        default      -> {
            int k = day.toString().length();
            int result = f(k);
            yield result;
        }
    };

Switch Expressions und ‘null’

Auch switch-Expressions sind bis Java 20 nicht in der Lage mit Null-Werten umzugehen (ausser das ab Java 17 verfügbare Preview Feature wird aktiviert). Wird dann einer switch-Expression null übergeben resultiert daraus eine NullPointerException. Ab Java 21 mit dem “Pattern Matching for switch” Feature können dann sowohl switch-Expressions als auch solche Statements mit Nullwerten umgehen sofern es eine case null -> bzw. case null : Klausel gibt. Ist dies nicht der Fall resultiert die Übergabe von null an switch weiterhin in einer NullPointerException.

break und continue in switch-Expressions

In switch-Expressions führt die Verwendung von break bzw. continue zu einem Compilefehler, da ansonsten ggf. kein Wert für die Expression ermittelt werden könnte. Wie weiter oben schon erwähnt können Blöcke in switch-Expressions unter Nutzung des yield Keyword und einer Expression die dann den Wert der switch-Expression bestimmt verlassen werden.

Erweiterungen von switch-Expressions mit Java 21

Mit Java 21 wurden sowohl JEP-441 (“Pattern matching for switch”) als auch JEP-440 (“Record Patterns”) final als Feature in die Sprache Java aufgenommen. Während “Pattern matching for switch” sich direkt auf die Erweiterung von switch-Statements/Expressions bezieht führt der JEP “Record Patterns” das Pattern Matching gegen Records ein mit der Möglichkeit Records anhand ihrer Struktur zu “zerlegen”, wobei dies zunächst nur im Kontext von Patterns für switch sowie Pattern Matching für instanceof möglich ist.

Im Einzelnen wurden folgende Erweiterungen vorgenommen:

  • Case-Label für switch wurden so erweitert, so daß auch vollständig qualifizierte Enum-Werte möglich sind und solche Case-Label mit anderen Arten von Case-Labels kombinierbar sind.
  • null wurde als zusätzlichen konstanten Case-Label zulassen
  • switch Selector-Expressions erlauben nun eine deutlich größere und flexiblere Anzahl an möglichen Typen die über die bisher möglichen Typen (ganzzahlig ausser long, Enum-Werte und Strings) deutlich hinausgehen.
  • Erweiterung von Case-Labeln um optionale when Klauseln die Prädikate darstellen mit denen Case-Zweige weiter differenziert werden können.

mit “Record Patterns” wird es darüber hinaus das Pattern-Matching zur Destrukturierung von Instanzen von Record-Klassen ermöglicht, um einfachere und/oder komplexere Zugriffe auf Records zu ermöglichen.

Im Folgenden soll ein Überblick über diese Features die mit Java 21 final zur Verfügung stehen gegeben werden.

Null-Werte

Wie weiter oben schon erwähnt ist es ab Java 21 möglich sowohl in switch-Statements als auch in switch-Expressions das Case-Label null zu nutzen, womit ein solches switch dann in der Lage ist mit Null-Werten umzugehen. Wird einem switch der kein null Label hat ein Null-Wert übergeben kommt es weiterhin zu einer NullPointerException. Das Case-Label null ist insofern speziell, als es sich lediglich mit dem default Label kombinieren lässt.

    boolean testIfString(Object o) {
        return switch (o) {
            case String s -> {
                System.out.println("Parameter is a String :" + o);
                yield true;
            }
            case null, default -> {
                System.out.println("Not a String :" + o);
                yield false;
            }
        };
    }

    static int parseIntNullAsZero(Object o) {
        return switch (o) {
            case null -> 0;
            case String s when isIntStr(s) -> Integer.parseInt(s);  // Implementierung von isIntStr(String) hier nicht angegeben
            default -> throw new IllegalArgumentException("Parameter is not a String or is not parseable as Integer.");
        };
    }

Um die Abwärtskompatiblität mit existierendem Code zu wahren matcht der Default-Zweig auch weiterhin null nicht

Patterns

Ab Java 21 ist es möglich neben konstanten Ausdrücken in Case-Labels auch Typ- und Record-Patterns zu nutzen. JEP-441 nennt dafür folgende Syntax:

SwitchLabel:
    case CaseConstant { , CaseConstant }
    case null [, default]
    case Pattern [ Guard ]
    default

Ob ein Case-Zweig mit einem Pattern ausgeführt wird oder nicht wird hier dann nicht mehr wie bisher durch einen Test auf Gleichheit festgestellt, sondern durch Pattern Matching, wobei hier Typ-Patterns als auch Record-Patterns möglich sind. Die Aufnahme von Pattern-Matching für switch führt dann auch dazu, dass für Selector-Expressions statt wie bisher Ganzzahltypen (ausser long), Strings und Enums nun zusätzlich beliebige Referenztypen möglich sind. Das folgende Beispiel zeigt ein switch mit null-Pattern als auch verschiedenen Typ-Patterns.

    // Beispiel aus JEP-441
    record Point(int i, int j) {}
    enum Color { RED, GREEN, BLUE; }

    static void typeTester(Object obj) {
        switch (obj) {
            case null     -> System.out.println("null");
            case String s when s.length > 0 -> System.out.println("String");
            case Color c  -> System.out.println("Color: " + c.toString());
            case Point p  -> System.out.println("Record class: " + p.toString());
            case int[] ia -> System.out.println("Array of ints of length" + ia.length);
            default       -> System.out.println("Something else");
        }
    }

Gültigkeitsbereich von in Patterns deklarierten Variablen

Typ-Patterns deklarieren Variable die an den entsprechend gecasteten Wert der Selector-Expression gebunden werden. Im Vorausgehenden Beispiel sind dies die Variablen s,c,p und ia.

Die genannten Variablen sind sowohl in einem ggf. vorhandenen when-Guard (s.u.) gültig, als auch auf der rechten Seite ihres Case-Zweiges. Definiert ein Case-Label mit Typ-Pattern eine Variable und wird die alte case L : Syntax verwendet, die ein “Fallthrough” Verhalten ermöglichen würde, so ist ein “Fallthrough” in diesem Fall nicht erlaubt.

Voll qualifizierte Enum-Werte

Wurde in einem switch bisher über Enum-Werte entschieden, so musste einerseits die Selector-Expression des switch vom Typ des Enum sein und andererseits die Case-Labels Werte aus diesem Enum sein. Beisp.:

    public String describeCurrentThreadState() {
        // Thread.State is an Enum
        Thread.State currentThreadState=Thread.currentThread().getState();
        return switch (currentThreadState) {
            case NEW -> "A thread that has not yet started is in this state";
            case RUNNABLE -> "A thread executing in the Java virtual machine is in this state";
            case BLOCKED -> "A thread that is blocked waiting for a monitor lock is in this state";
            case WAITING -> "A thread that is waiting indefinitely for another thread to perform a particular action is in this state";
            case TIMED_WAITING -> "A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state";
            case TERMINATED -> "A thread that has exited is in this state.";
        };
    }

Jetzt kann der Typ der Selector Expression beliebig sind, daß heisst es kann über beliebige Typen entschieden werden. Kommen in den Case Zweigen unterschiedliche Enums vor, dann ist es trotzdem möglich ohne Type Patterns auszukommen weil nun voll qualifizierte Enum-Werte möglich sind und es sogar möglich ist in einem switch Case-Zweige mit Enum-Werten und andere zu mischen. Hierbei ist darauf zu achten, daß die Selector-Expression ein Supertyp der in den Case-Zweigen verwendeten Typen sein muss.

    sealed interface State permits ActiveState, InactiveState, SpecialState {}
    enum ActiveState implements State { WORKING, WAITING }
    enum InactiveState implements State { STARTING, STOPPING, TERMINATED }
    
    final class SpecialState implements State {
        final public String desc;
        final public boolean urgent;
        
        public SpecialState(String desc, boolean urgent) {
            this.desc=desc; this.urgent=urgent;
        }
    }

    State s=getState();
    String desc=switch (s) {
      case ActiveState.WORKING -> "actively working";
      case ActiveState.WAITING -> "activelty waiting";
      case InactiveState.STARTING -> "starting";
      case InactiveState.STOPPING -> "stopping";
      case InactiveState.TERMINATED -> "terminated";
      case SpecialState state when state.urgent -> "unknown urgent state : %s".formatted(state.desc);
      case SpecialState state -> "unkown state : %s".formatted(state.desc);
    };
Record Patterns

Auch switch unterstützt Record-Patterns. Hiermit ist es möglich Records schon beim Pattern-Matching für Switch in ihre Bestandteile zu zerlegen.

    record SeverityAndAction(int severity,String Action) {}

    SeverityAndAction action=getNextAction();
    switch (action) {
      case SeverityAndAction(int s,String a) when s < 10 -> System.out.println("Not Severe! %s".formatted(a));
      case SeverityAndAction(int s,String a) when s > 20 -> System.out.println("Very severe. Urgent! %s".formatted(a));
      case SeverityAndAction(int s,String a) -> System.out.println("Severe. %s".formatted(a));
    }

SeverityAndAction(int s,String a) ist hierbei ein Record-Pattern, das für die Bestandteile des Records eigene lokale Variablen deklariert (hier s und a) und diesen die Werte der entsprechenden Member des Record zuweist.

when als ‘Guard’ für Patterns

Guards sind zusätzliche Bedingungen in Form von Prädikaten und sind nur nach Patterns erlaubt.

    Runnable mkHandlerFor(Object url) {
        return switch (url) {
            case String s when s.startsWith("http://") -> mkHttpHandler(s);
            case String s when s.startsWith("file://") -> mkFileHandler(s);
            case URL u when u.getProtocol().equals("http") -> mkHttpHandler(u.toExternalForm());
            case URL u when u.getProtocol().equals("file") -> mkFileHandler(u.toExternalForm());
            null -> DEFAULT_HANDLER;
            default -> throw new IllegalArgumentException("Unknown URL-Scheme");
        };
    }

Das Schlüsselwort when leitet den optionalen Guard eines Case-Pattern ein und ist ein Boolescher Ausdruck, der ausgewertet wird falls das Pattern matcht. Ist der Guard ‘true’ kommt die rechte Seite des case zum zuge, andernfalls werden die nachfolgenden Case-Zweige geprüft.

Case Label Dominanz

Mit der Einführung von Pattern Matching für switch kann es vorkommen, das mehr als ein Pattern auf einen Wert matcht der einem switch übergeben wurde. Die Case-Labels werden der Reihenfolge nach auf Übereinstimmung getestet und es kann vorkommen, daß ein Case-Zweig immer zuerst vor einem anderen übereinstimmt und ihn sozusagen “überdeckt”.

Folgendes Beispiel aus JEP-441 verdeutlich dieses als “Case label dominance” benannte Verhalten.

    static void error(Object obj) {
        switch(obj) {
            case CharSequence cs ->
                System.out.println("A sequence of length " + cs.length());
            case String s -> // error: this case label is dominated by a preceding case label
                System.out.println("A string: " + s);
            default -> { break; }
        }
    }

In diesem Fall kommt es zu einem Compile-Fehler “Label is dominated by a preceeding case label ‘CharSequence cs’”.

Für Patterns mit when Klauseln kann der Compiler nicht entscheiden ob ein Pattern ein anderes “dominiert”, so daß für Patterns mit when Klausel nicht geprüft wird, ob sie ein anderes “dominieren”. Im vorliegenden Fall würde, also z.b. ein when !cs.isEmpty() || cs.isEmpty() dazu führen, daß der Compile-Fehler verschwindet, obwohl sich logisch am Code nichts ändert und dieses Case-Label weiterhin den zweiten Case-Zweig dominiert. JEP-441 empfiehlt daher die Case-Labels wie folgt zu sortieren:

  • konstante Case-Labels
  • Patterns mit when
  • Patterns ohne when

Fazit

Die mit Java 14 als Feature eingeführten Switch-Expressions sind eine Verbesserung der vorhandenen Switch-Statements. Sie ermöglichen die Verwendung von Switch-Anweisungen als Ausdrücke und verbessern die Code-Lesbarkeit und Wartbarkeit. Das zuvor fehlerträchtige “Fall-Through” wird mit Switch-Expressions entschärft indem ein “Fall-Through” in Case-Expressions nicht erlaubt ist, dafür aber mehrere Werte in einem Case-Zweig geprüft werden können.
Mit Java 21 kommen weitere Verbesserungen wie das “Pattern Matching for switch” und “Record Patterns” hinzu. Diese Features verbessern die Ausdruckstärke sowohl von Switch-Expressions als auch von Switch-Statements deutlich und ermöglichen es komplexe Logik deutlich kompakter und eleganter umzusetzen.