🚀AdvancedModern Java

Pattern Matching for Switch

Use switch expressions with type patterns, guarded patterns, and deconstruction for expressive dispatch logic.

Overview

Pattern matching for switch (finalized in Java 21 as part of JEP 441) extends the switch expression to support type patterns, guarded patterns, and null handling. Combined with sealed classes and records, it enables exhaustive, concise dispatch that replaces long if-instanceof-cast chains. Before Java 21, handling a sealed hierarchy required cascading if (obj instanceof SubTypeA a) { ... } else if (obj instanceof SubTypeB b) { ... }. With switch patterns, the same logic becomes a clean switch expression with the compiler verifying that all cases are covered (exhaustiveness checking). Pattern matching for switch is one of the most significant additions to Java's type system in recent years.

Key Points

  • •switch(obj) { case TypeA a -> ...; case TypeB b -> ...; } matches by type
  • •Guarded patterns: case TypeA a when a.value() > 0 -> ... adds conditions to patterns
  • •null case: case null -> ... handles null explicitly in switch (previously caused NPE)
  • •default covers all remaining cases; not needed if the switch is exhaustive over a sealed hierarchy
  • •The compiler checks exhaustiveness for switches over sealed types
  • •Deconstruction patterns: case Point(int x, int y) -> x + y (available with records)
  • •switch expressions return a value; no fall-through between arrow cases
  • •Dominance rule: more specific patterns must come before more general ones

Examples

Type pattern switch over a sealed hierarchy

public class Main {
    sealed interface Shape permits Circle, Rectangle, Triangle {}
    record Circle(double radius) implements Shape {}
    record Rectangle(double width, double height) implements Shape {}
    record Triangle(double base, double height) implements Shape {}

    static double area(Shape shape) {
        return switch (shape) {
            case Circle c       -> Math.PI * c.radius() * c.radius();
            case Rectangle r    -> r.width() * r.height();
            case Triangle t     -> 0.5 * t.base() * t.height();
        };
        // No default needed: sealed hierarchy is exhaustive
    }

    public static void main(String[] args) {
        System.out.println(area(new Circle(5)));          // 78.5...
        System.out.println(area(new Rectangle(4, 6)));    // 24.0
        System.out.println(area(new Triangle(3, 8)));     // 12.0
    }
}

Because Shape is sealed, the compiler knows all subtypes and verifies every case is handled. Adding a new subclass to the sealed interface without updating the switch produces a compile error, not a silent bug.

Guarded patterns with when clause

public class Main {
    sealed interface Discount permits FlatDiscount, PercentDiscount, NoDiscount {}
    record FlatDiscount(double amount) implements Discount {}
    record PercentDiscount(double rate) implements Discount {}
    record NoDiscount() implements Discount {}

    static double apply(Discount discount, double price) {
        return switch (discount) {
            case FlatDiscount d when d.amount() >= price -> 0.0;  // free
            case FlatDiscount d                          -> price - d.amount();
            case PercentDiscount d when d.rate() >= 1.0 -> 0.0;  // 100%+ off
            case PercentDiscount d                       -> price * (1 - d.rate());
            case NoDiscount d                            -> price;
        };
    }

    public static void main(String[] args) {
        System.out.println(apply(new FlatDiscount(150), 100)); // 0.0 (capped)
        System.out.println(apply(new FlatDiscount(20), 100));  // 80.0
        System.out.println(apply(new PercentDiscount(0.1), 100)); // 90.0
    }
}

Guarded patterns refine a type match with an additional condition. More specific (guarded) patterns must appear before more general ones. If two cases have the same type, the guarded one comes first.

Handling null and using default in switch

public class Main {
    static String describe(Object obj) {
        return switch (obj) {
            case null          -> "null value";
            case Integer i
                when i < 0     -> "negative integer: " + i;
            case Integer i     -> "positive integer: " + i;
            case String s
                when s.isEmpty() -> "empty string";
            case String s      -> "string: " + s;
            default            -> "other: " + obj.getClass().getSimpleName();
        };
    }

    public static void main(String[] args) {
        System.out.println(describe(null));
        System.out.println(describe(-5));
        System.out.println(describe(42));
        System.out.println(describe(""));
        System.out.println(describe("hello"));
        System.out.println(describe(3.14));
    }
}

A switch over Object needs default because it is not sealed. The case null pattern handles null explicitly instead of throwing NullPointerException as traditional switch would.

Common Mistakes

  • Putting a more general pattern before a specific one. For example, case Number n before case Integer i would make Integer unreachable (dominated). The compiler reports a compile error.
  • Omitting null handling and passing null to a switch. Without case null, passing null throws NullPointerException. Explicitly handle it or document that null is not allowed.
  • Confusing arrow switch expressions with colon switch statements. Arrow cases (->) do not fall through; colon cases (:) do. Mixing is not allowed.
  • Assuming exhaustiveness for interfaces not sealed. For non-sealed interfaces, you must add a default case or the compiler will reject the switch as non-exhaustive.

Note

Pattern matching switch works best alongside sealed interfaces and records. The trio creates an algebraic data type (ADT) system in Java: sealed defines the variants, records define the data, and switch deconstructs them exhaustively. This is the Java equivalent of pattern matching in Haskell, Rust, or Kotlin.

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 2 exercises

Knowledge Check

Question 1 of 4

What is required for a switch over a sealed interface to be considered exhaustive (no default needed)?