Java’s pattern matching for switch, finalized in Java 21, lets you test a value’s type and deconstruct it directly in a switch. It reduces boilerplate instanceof checks, improves readability, and enables safer, more expressive code. As of Monday, November 10, 2025 (UTC), this feature is stable in mainstream JDKs, so you can use it in production without preview flags.Key benefits:
- Cleaner code by combining type testing and extraction.
- Exhaustive switching with sealed hierarchies—often without a
default. - Guards (the
whenclause) for refined, readable conditions. - Built-in null handling in switch.
SEO keywords to know: Java pattern matching for switch, Java 21 switch patterns, guarded patterns, record patterns, sealed classes, switch expression.
Getting Started: Basic switch Patterns
Pattern matching allows different actions depending on the runtime type. You can also use switch expressions to return values concisely.
static String describe(Object obj) {
return switch (obj) {
case Integer i -> "Integer: " + i;
case String s when s.isBlank() -> "Blank String";
case String s -> "String: " + s;
case null -> "It’s null";
default -> "Unknown type: " + obj.getClass().getSimpleName();
};
}
Why this is better:
- No manual casts; the variable
iorsis already typed. - Guards like
when s.isBlank()let you branch cleanly. - Null-safe: you can explicitly handle
nullin the switch.
Guarded Patterns: Add Conditions Inline
Guards use when to add an extra boolean condition to a case. This keeps branching logic in one place, reducing nested if statements.
static String classifyNumber(Number n) {
return switch (n) {
case Integer i when i < 0 -> "Negative int";
case Integer i when i == 0 -> "Zero int";
case Integer i -> "Positive int";
case Long l when l > 1_000_000L -> "Large long";
case Double d when Double.isNaN(d) -> "NaN double";
default -> "Some other number";
};
}
Best practice: Order guarded cases from most specific to least specific to avoid unreachable branches.
Handling null in switch
Unlike older Java, pattern matching for switch lets you match null explicitly. This encourages safer code and avoids accidental NPEs.
static String safeToString(Object o) {
return switch (o) {
case null -> "<null>";
case String s -> s;
default -> o.toString();
};
}
Tip: If you omit case null, and o is null, the switch will throw a NullPointerException. Add a case null when nulls are possible.
Exhaustive Switches with Sealed Hierarchies
When you use a sealed interface or class, the compiler knows all permitted subtypes. Your switch can become exhaustive without a default, improving safety during future refactors.
sealed interface Shape permits Circle, Rectangle, Square {}
record Circle(double r) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
record Square(double s) implements Shape {}
static double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.r() * c.r();
case Rectangle r -> r.w() * r.h();
case Square sq -> sq.s() * sq.s();
};
}
Why this is great:
- No default needed; if you add a new subtype, the compiler flags missing cases.
- Refactor-friendly and type-safe.
Record Patterns Inside switch
Record patterns (final in Java 21) pair perfectly with switch patterns for deconstruction and matching.
record Point(int x, int y) {}
record Line(Point p1, Point p2) {}
static String describeLine(Object o) {
return switch (o) {
case Line(Point(int x1, int y1), Point(int x2, int y2))
when x1 == x2 -> "Vertical line";
case Line(Point p1, Point p2) -> "Line from %s to %s".formatted(p1, p2);
default -> "Not a line";
};
}
You can nest record patterns and use guards to express domain logic succinctly.
Dominance and Reachability: Avoid Common Pitfalls
The compiler checks that no case is dominated by a previous, more general case.Example of a compile-time error:
static String bad(Object o) {
return switch (o) {
case CharSequence cs -> "CharSequence";
case String s -> "String"; // Error: dominated by CharSequence
default -> "Other";
};
}
Fix by reversing order or adding guards:
static String good(Object o) {
return switch (o) {
case String s -> "String";
case CharSequence cs -> "CharSequence";
default -> "Other";
};
}
Rule of thumb: Put more specific patterns first, then broader ones.
Best Practices and Migration Tips
- Target Java 21+ to use pattern matching for switch and record patterns without preview flags.
- Prefer switch expressions when you compute and return a value; they are concise and reduce mutable state.
- Handle null explicitly if nulls may appear in your domain.
- Leverage sealed hierarchies to achieve exhaustiveness and catch omissions at compile time.
- Use guards sparingly but purposefully to keep matching logic readable.
- Avoid overly broad types before specific ones to prevent dominance issues.
Wrap-Up
Java’s pattern matching for switch in Java 21+ brings concise, expressive, and safer branching to everyday code. With guarded patterns, record deconstruction, and sealed-type exhaustiveness, you can replace verbose instanceof chains with readable, maintainable switches. Adopt it in new code, incrementally refactor older code paths, and enjoy the compile-time checks and clarity this modern Java feature provides.
