Pattern Matching for Switch
Use switch expressions with type patterns, guarded patterns, and deconstruction for expressive dispatch logic.
Overview
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.
Completed: 0 / 2 exercises
Knowledge Check
Question 1 of 4