Java 21 Pattern Matching for switch with Examples

Introduction Until Java 21, a switch could only choose between constant values of a few types - integers, strings and enums. To branch on the type of an object, you wrote a chain of if (obj instanceof ...) tests. Pattern matching for switch, previewed four times from Java 17 to 20 and final in Java 21Click to read tutorial: Overview of New Features in Java 21 (JEP 441), lets switch work with any type, with patterns in its case labels. It builds on switch expressionsClick to read: switch expressions (Java 14) and pattern matching for instanceofClick to read: pattern matching for instanceof (Java 16). This tutorial explains pattern matching for switch with examples - type patterns, when guards, handling null, exhaustiveness, and the dominance rules the compiler enforces. Type patterns in case labels
Replacing an if-instanceof chain with switch
//SwitchPatternsDemo.java
import java.util.List;

public class SwitchPatternsDemo {
  static String formatOld(Object obj) {                     // before Java 21
    if (obj instanceof Integer i) return "int " + i;
    else if (obj instanceof Long l) return "long " + l;
    else if (obj instanceof String s) return "String \"" + s + "\"";
    else return "other " + obj;
  }

  static String format(Object obj) {                        // Java 21
    return switch (obj) {
      case Integer i -> "int " + i;
      case Long l -> "long " + l;
      case String s -> "String \"" + s + "\"";
      case int[] arr -> "int array of length " + arr.length;
      default -> "other " + obj;
    };
  }

  public static void main(String[] args) {
    System.out.println(formatOld(42));
    for (Object o : List.of(42, 7L, "Java", new int[] {1, 2}, 3.14)) {
      System.out.println(format(o));
    }
  }
}
 OUTPUT of the above code
int 42
int 42
long 7
String "Java"
int array of length 2
other 3.14
Each case label is a type pattern, exactly as in instanceof: if the object has that type, it is bound to the variable and the case runs. The switch is clearer than the if chain, and the compiler can check it, as we will see.
Guards with when, and handling null A case can add a condition with when, called a guard. And while a traditional switch throws a NullPointerException when the selector is null, a switch can now handle it with case null -
Guarded patterns and case null
//GuardsAndNull.java
public class GuardsAndNull {
  static String classify(Object obj) {
    return switch (obj) {
      case null -> "nothing";
      case String s when s.isBlank() -> "a blank string";
      case String s when s.length() > 10 -> "a long string";
      case String s -> "a string: " + s;
      case Integer i when i < 0 -> "a negative number";
      case Integer i -> "a number: " + i;
      default -> "something else";
    };
  }

  public static void main(String[] args) {
    for (Object o : new Object[] {null, "  ", "pattern matching", "Java", -5, 21, 2.5}) {
      System.out.println(classify(o));
    }
  }
}
 OUTPUT of the above code
nothing
a blank string
a long string
a string: Java
a negative number
a number: 21
something else
Cases are tried from top to bottom, so the guarded String cases come before the unguarded one. case null can also be combined with default as case null, default -> .... Without a case null, a null selector still throws a NullPointerException, as before. Exhaustiveness with sealed types and enums A switch with patterns must be exhaustive: every possible value must be handled. Over Object that requires a default. But over a sealedClick to read: sealed type, covering every permitted subtype is enough - and leaving one out is a compile error -
An exhaustive switch over a sealed interface
//ExhaustiveSwitch.java
public class ExhaustiveSwitch {
  sealed interface Shape permits Circle, Square, Rect { }
  record Circle(double r) implements Shape { }
  record Square(double side) implements Shape { }
  record Rect(double w, double h) implements Shape { }

  static double area(Shape s) {
    return switch (s) {
      case Circle c -> Math.PI * c.r() * c.r();
      case Square sq -> sq.side() * sq.side();
      case Rect(double w, double h) -> w * h;        // a record pattern works too
    };                                               // no default: all permitted types covered
  }

  public static void main(String[] args) {
    for (Shape s : new Shape[] {new Circle(1), new Square(2), new Rect(2, 3)}) {
      System.out.printf("%s -> %.2f%n", s, area(s));
    }
  }
}
 OUTPUT of the above code
Circle[r=1.0] -> 3.14
Square[side=2.0] -> 4.00
Rect[w=2.0, h=3.0] -> 6.00
Leaving out the Rect case is reported by the compiler -
A switch which misses a permitted type
//NotExhaustive.java
public class NotExhaustive {
  sealed interface Shape permits Circle, Square { }
  record Circle(double r) implements Shape { }
  record Square(double side) implements Shape { }

  static String name(Shape s) {
    return switch (s) {
      case Circle c -> "circle";
    };
  }
}
 OUTPUT on compiling the above code
NotExhaustive.java:8: error: the switch expression does not cover all possible input values
    return switch (s) {
           ^
1 error
This is the real benefit of combining sealed types with switch: when a new subtype is added, the compiler finds every switch which must handle it. If a class is added to a hierarchy without recompiling such a switch, the JVM throws a MatchException at runtime rather than silently doing nothing.
Dominance A case label dominates a later one if every value matching the later label would already match the earlier one - for example case CharSequence cs before case String s, or an unguarded pattern before a guarded pattern of the same type. The later case could never run, so the compiler reports an error -
A case label dominated by an earlier one
//Dominance.java
public class Dominance {
  static String check(Object obj) {
    return switch (obj) {
      case CharSequence cs -> "char sequence";
      case String s -> "string";              // never reached: every String is a CharSequence
      default -> "other";
    };
  }
}
 OUTPUT on compiling the above code
Dominance.java:6: error: this case label is dominated by a preceding case label
      case String s -> "string";              // never reached: every String is a CharSequence
           ^
1 error
Java 21 also allows qualified enum constants in case labels, such as case Color.RED, so that a switch over a sealed interface can mix enum constants and record patterns. Summary In this tutorial we saw how pattern matching for switch, final in Java 21, lets switch branch on the type of any object. We replaced an if-instanceof chain with type patterns in case labels, refined cases with when guards, and handled null with case null. We then wrote an exhaustive switch over a sealed interface with no default, and saw the compiler errors for a missing case and for a dominated case. Next, read about sequenced collectionsClick to read tutorial: Java 21 Sequenced Collections Explained.