2.3 Pattern Matching for switch and instanceof
Key Takeaways
- Pattern matching for instanceof binds a typed pattern variable upon a successful type test, with scope determined by compiler flow analysis.
- Pattern matching for switch (finalized in Java 21 via JEP 441) allows switching over any reference type using type patterns, record patterns, and explicit null labels.
- Guarded patterns utilize the when clause to attach boolean conditions to pattern cases without nested if statements.
- Pattern dominance is strictly enforced by the compiler: general supertypes and total patterns must not appear prior to more specific subtype patterns.
- Exhaustiveness in pattern matching switch is satisfied when all possible hierarchy types (e.g., in sealed classes) or a total pattern/default case are handled.
Pattern Matching for switch and instanceof
Pattern matching simplifies Java code by combining type testing, casting, and local variable binding into a single declarative operation. Java 16 standardized pattern matching for instanceof (JEP 394), and Java 21 finalized pattern matching for switch (JEP 441) along with Record Patterns (JEP 440). These features form a central pillar of the 1Z0-830 certification exam.
1. Pattern Matching for instanceof
The Problem Pattern Matching Solves
In legacy Java, checking a type and accessing its specific members required a three-step boilerplate pattern: test with instanceof, explicitly cast to the target type, and assign to a new local variable.
// Legacy Java approach:
if (obj instanceof String) {
String s = (String) obj; // Manual explicit cast
System.out.println(s.toUpperCase());
}
// Modern Pattern Matching approach:
if (obj instanceof String s) {
System.out.println(s.toUpperCase()); // 's' is automatically cast and bound!
}
Flow Scoping Rules
Pattern variables do not follow standard lexical block scoping; instead, they use flow scoping. A pattern variable is in scope only on the execution paths where the compiler can guarantee the pattern has matched (definitely assigned).
- Logical AND (
&&) Scoping: The right operand is only evaluated if the left operand evaluates totrue. Therefore, the pattern variable is in scope on the right side of&&:if (obj instanceof String s && s.length() > 5) { // LEGAL: s is in scope on right side of && System.out.println(s.toUpperCase()); } - Logical OR (
||) Scoping: The right operand is evaluated when the left operand evaluates tofalse. Therefore, the pattern variable is NOT in scope on the right side of||:// if (obj instanceof String s || s.length() > 5) { } // COMPILE ERROR: cannot find symbol 's' - Negation and Early Return Scoping: If an
ifstatement tests!(obj instanceof String s)and terminates with areturn,throw, orbreak,sremains in scope for the rest of the method:void printLength(Object obj) { if (!(obj instanceof String s)) { return; // Exits if NOT a String } // 's' is in scope here because execution only reaches here if obj WAS a String! System.out.println("String length: " + s.length()); } - Shadowing and Scope Clashes: A pattern variable cannot have the same name as an existing local variable in the same scope, but it may shadow a class field.
2. Pattern Matching for switch (Java 21 JEP 441)
In Java 21, the selector expression of a switch construct can be any reference type, and case labels can be type patterns, record patterns, constants, or null.
static String formatValue(Object obj) {
return switch (obj) {
case Integer i -> String.format("Integer: %d", i);
case Long l -> String.format("Long: %d", l);
case Double d -> String.format("Double: %.2f", d);
case String s -> String.format("String: '%s'", s);
case null -> "Null reference";
default -> obj.toString();
};
}
3. Guarded Patterns with the when Clause
A guarded pattern allows developers to specify a conditional boolean expression on a case label using the when contextual keyword (replacing the preview && syntax):
static void classify(Object obj) {
switch (obj) {
case String s when s.length() > 10 -> System.out.println("Long string: " + s);
case String s when !s.isEmpty() -> System.out.println("Non-empty string: " + s);
case String s -> System.out.println("Empty string");
case Integer i when i > 0 -> System.out.println("Positive int: " + i);
case Integer i -> System.out.println("Zero or negative int: " + i);
default -> System.out.println("Other object");
}
}
Guard Expression Rules:
- The expression following
whenmust evaluate to abooleanorBoolean. - The pattern variable declared in the case label is in scope within the
whenexpression. - If the type matches but the
whenexpression evaluates tofalse, execution skips this case and tests subsequent cases.
4. Pattern Dominance and Ordering Rules
The compiler checks case patterns top-to-bottom and enforces strict dominance rules. If a preceding case pattern matches a superset of all values that a subsequent pattern can match, the preceding pattern dominates the subsequent one, causing a compile-time error for unreachable code.
Dominance Ordering Hierarchy:
- Specific Guarded Pattern must precede General Unguarded Pattern:
// INVALID: Compilation Error! switch (obj) { case String s -> System.out.println("Any string"); case String s when s.length() > 5 -> System.out.println("Long string"); // ERROR: Dominated! } // CORRECT ORDER: switch (obj) { case String s when s.length() > 5 -> System.out.println("Long string"); case String s -> System.out.println("Any string"); } - Subtype Pattern must precede Supertype Pattern:
// INVALID: Compilation Error! switch (obj) { case CharSequence cs -> System.out.println("CharSequence"); case String s -> System.out.println("String"); // ERROR: Dominated by CharSequence! } // CORRECT ORDER: switch (obj) { case String s -> System.out.println("String"); case CharSequence cs -> System.out.println("CharSequence"); } - Total Patterns and Default:
A pattern
case Object ois a total pattern for anyObjectselector. A total pattern matches all non-null objects and dominates all subsequent type patterns, meaning it must appear last.
5. Null Handling in Pattern Matching Switch
Historically, passing null to a switch statement immediately triggered a NullPointerException. In Java 21, nullability is explicitly integrated into pattern matching:
- Explicit
case null: Handlesnullvalues cleanly without throwing an exception.switch (obj) { case null -> System.out.println("Object is null"); case String s -> System.out.println("String: " + s); default -> System.out.println("Other"); } - Combined
case null, default: Allows handlingnulland unmatched objects in a single fallback branch:switch (obj) { case String s -> System.out.println("String: " + s); case null, default -> System.out.println("Null or unsupported type"); } - Implicit Null Failure: If the switch selector evaluates to
nulland neithercase nullnorcase null, defaultis present, the switch throws aNullPointerExceptionbefore evaluating any patterns (even if an unguardeddefaultis present!).
6. Record Patterns and Deconstruction (JEP 440)
Java 21 allows deconstructing record components directly inside pattern matching:
record Point(int x, int y) {}
record Line(Point start, Point end) {}
static void printPoint(Object obj) {
if (obj instanceof Point(int x, int y)) {
System.out.println("Point at: " + x + ", " + y);
}
}
static void printLine(Object obj) {
if (obj instanceof Line(Point(int x1, int y1), Point(var x2, var y2))) {
System.out.printf("Line from (%d,%d) to (%d,%d)%n", x1, y1, x2, y2);
}
}
7. Exhaustiveness and Sealed Hierarchies
A pattern matching switch expression must be exhaustive. When switching over a sealed class or interface, if all permitted direct subtypes are covered by case patterns, the switch expression is exhaustive without requiring a default label:
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
record Triangle(double b, double h) implements Shape {}
double getArea(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.w() * r.h();
case Triangle t -> 0.5 * t.b() * t.h();
// No default required! All permitted direct subtypes of Shape are handled.
};
}
If a new subtype is added to the Shape interface later, the compiler immediately flags the switch expression with a compile-time error, preventing missing branch bugs across codebases.
What is the result of attempting to compile and run the following method?
public static void check(Object obj) {
if (!(obj instanceof String s)) {
System.out.print("Not String ");
return;
}
System.out.print(s.toUpperCase());
}
Given the following switch expression, why does the code fail to compile?
public static String evaluate(CharSequence cs) {
return switch (cs) {
case CharSequence c -> "Any CharSequence";
case String s -> "String: " + s;
};
}
What is the result of executing the method call process("JAVA")?
public static void process(Object obj) {
switch (obj) {
case String s when s.length() < 3 -> System.out.print("Short ");
case String s when s.length() >= 4 -> System.out.print("Long ");
case String s -> System.out.print("Medium ");
case null -> System.out.print("Null ");
default -> System.out.print("Other ");
}
}
Given the following sealed hierarchy declarations:
Which of the following switch expressions fails to compile?sealed interface Shape permits Circle, Square {}
final class Circle implements Shape {}
final class Square implements Shape {}