Java Switch Pattern Matching
java switch pattern matching: Pattern matching for switch simplifies type checks and extraction in Java. Learn syntax, guarded patterns, null handling, and when to use...
Pattern matching for switch, introduced as a preview in Java 17 and finalized in Java 21, changes how developers write type-based branching. Instead of a chain of if statements combined with instanceof checks and manual casts, you can now select a branch based on a pattern that also binds variables. This article explains the syntax, practical usage, and important limitations of java switch pattern matching.
The Core Syntax and a Minimal Example
A switch statement or expression can now use type patterns as case labels. Consider a method that processes an object that could be a String, an Integer, or a null. Without pattern matching, you would write an if chain with instanceof and casts:
Object obj = ...; if (obj instanceof String s) { System.out.println(s.length()); } else if (obj instanceof Integer i) { System.out.println(i + 1); } else { System.out.println("Unknown type"); }
With pattern matching for switch, the same logic becomes a switch expression:
Object obj = ...; String result = switch (obj) { case String s -> "Length: " + s.length(); case Integer i -> "Value: " + (i + 1); default -> "Unknown type"; };
Each case checks whether the selector expression matches the pattern. If it matches, the bound variable (s or i) is already cast and available. There is no separate cast step. This reduces boilerplate and keeps related branches close together.
Why This Improves Readability and Maintainability
The if-instanceof chain works, but it becomes harder to read as the number of types grows. Each branch must repeat the instanceof check, the cast, and the variable declaration. With pattern matching, the pattern itself conveys the type and binding. This is especially visible when you handle a sealed hierarchy, where the compiler can often verify that you have covered all possible subtypes.
Consider a sealed interface Shape with records Circle and Rectangle. Using pattern matching for switch, you can write:
sealed interface Shape permits Circle, Rectangle {} record Circle(double radius) implements Shape {} record Rectangle(double width, double height) implements Shape {} double area(Shape shape) { return switch (shape) { case Circle c -> Math.PI * c.radius() * c.radius(); case Rectangle r -> r.width() * r.height(); }; }
Because Shape is sealed, the compiler knows there are only two possible subtypes, and you do not need a default branch. If you later add a new permitted subtype, the switch will no longer compile until you handle it. That kind of compile-time safety is impossible with an if chain. For an open interface, a default branch remains necessary.
Guarded Patterns and Fine-Grained Conditions
A type pattern alone matches all values of that type. If you need additional conditions, you can use a guarded pattern with the && operator. Example:
String classify(Integer i) { return switch (i) { case Integer n && n > 0 -> "positive"; case Integer n && n < 0 -> "negative"; case Integer n -> "zero"; }; }
The guard n > 0 is evaluated only after the pattern Integer n matches. If the guard is false, the switch continues to the next case. Guards are useful when you want to combine type matching with a specific condition without falling back to nested if statements. Note that the guard must not have side effects that matter to program logic; it should be a pure condition.
Handling Null Explicitly
In traditional switch statements, passing null to the selector throws a NullPointerException unless you check for null beforehand. With pattern matching for switch, you can handle null directly as a case:
String describe(Object obj) { return switch (obj) { case null -> "null"; case String s -> "String: " + s; case Integer i -> "Integer: " + i; default -> "Other"; }; }
This is more direct than adding a separate null check before the switch. However, the null case must come before any type pattern that could match null? Actually, type patterns do not match null, so the order matters when a guard or a more specific pattern is present. In practice, putting the null case first is conventional and avoids ambiguity. The compiler enforces that case null is allowed and that a default branch can appear at most once.
Runtime Behavior and Performance Considerations
Pattern matching for switch is syntactic sugar that the compiler translates into bytecode that behaves similarly to an if-instanceof chain, but there are subtle differences. The compiler may optimize certain patterns, for example by ordering type checks to avoid repeated casts. In general, the runtime cost is comparable to a series of type checks. You should not avoid pattern matching for performance reasons without first profiling. For most application code, the readability and safety benefits outweigh any micro-optimizations you might achieve with manual casts.
However, there is a performance consideration that is not micro: When you use a default branch that does nothing, the runtime still must evaluate all previous patterns. If you have a long list of types, each match is a linear scan. This is no different from a chain of else if. If you have a very large number of types and the matching is on a hot path, you might consider restructuring the code, for example by using a Map of functions or a polymorphic dispatch. But for typical domain models with a handful of types, the switch is clear and efficient enough.
Common Mistakes and How to Avoid Them
One common mistake is to write a pattern that is impossible to match, leading to a compile-time error. For example:
switch (obj) { case String s -> ...; case CharSequence cs -> ...; // Compile error: CharSequence is a supertype of String }
The compiler rejects a pattern that is already covered by an earlier, more specific pattern. Similarly, you cannot have two identical type patterns. This is a feature: it forces you to order cases from most specific to least specific, preventing accidental shadowing.
Another mistake is to use a guarded pattern that is too broad, such as case String s && true. That is redundant. Also, be wary of guards that throw exceptions. If a guard throws, the switch behaves as if the exception happened naturally; it does not fall through to the next case. For maintainability, keep guards side-effect-free and simple.
Compatibility and Version Requirements
Pattern matching for switch is not available in earlier Java versions. To use it, you must compile with a Java version that supports it. In Java 17 and 18, it was a preview feature; you had to enable preview using --enable-preview. In Java 21, it became a final feature, so no preview flag is required. If you are using an older version, you cannot directly use this syntax. You can either upgrade the Java runtime or stick with the traditional instanceof chain. Libraries compiled with this feature require a JVM that can load class files with the new bytecode, so you need to check your deployment environment.
Additionally, the exact behavior of case null and type patterns depends on the Java version. Always test on the target runtime. For code that must run on Java 11 or 8, this feature is simply unavailable, so plan accordingly.
When Pattern Matching for Switch Is Not the Right Choice
Pattern matching for switch is excellent for type-based dispatch, but it is not a universal replacement for all switch usage. If your switch is only testing integer constants or enums, the traditional case with constants is still appropriate. Pattern matching adds no value there. Also, if you need to check multiple conditions on the same value that do not reduce to a single type pattern, a series of if statements might be clearer.
Another scenario is when you want to match a pattern that depends on runtime data that is not the selector itself. For example, matching a Map entry based on a key type is not directly supported. You would need to decompose the map into a record first. In general, pattern matching works best with sealed hierarchies and records, or when you are handling a loosely typed object such as a JSON value where each type has distinct semantics.
A maintainability tradeoff: Adding a new case to a pattern-matching switch is straightforward, but removing or reordering cases can cause subtle compiler errors if you forget that a supertype pattern covers a subtype. Rely on the compiler to warn you about dominance, and use sealed hierarchies to get exhaustiveness checks. That yields a switch that the compiler keeps honest as your domain model evolves.