Pattern Matching in Java
Introduction
For decades, Java required developers to use a boilerplate-heavy pattern: check the type of an object using instanceof, cast the object to that type, and finally extract data from it.
Pattern Matching, introduced gradually through a series of JEPs (Java Enhancement Proposals) starting in Java 14, revolutionizes this workflow. It allows developers to express complex data queries concisely and safely by combining type testing, casting, and variable extraction into a single, cohesive operation.
Core Concept: Deep Dive and Architecture
The Evolution of Pattern Matching
- Pattern Matching for
instanceof(Java 16): Eliminates the need for explicit casting after aninstanceofcheck. - Pattern Matching for
switch(Java 21): Extends switch statements to match on types and extract data, moving away from simple equality checks. - Record Patterns (Java 21): Allows deep deconstruction of Record classes directly in the matching clause.
How It Works Internally
Pattern matching introduces the concept of a pattern variable. When the JVM evaluates a pattern match, it essentially performs:
- A Predicate Test: Does the target object match the pattern (e.g., is it an instance of a specific class)?
- Conditional Binding: If the test succeeds, a new implicitly typed, implicitly
finalvariable (the pattern variable) is bound to the target object and is immediately available in the scope of the successful branch.
Flow Scoping
The scope of a pattern variable is determined by Flow Scoping. The variable is only in scope where the compiler can definitively prove that the pattern matched. This prevents NullPointerExceptions and ClassCastExceptions at compile time.
Code Examples
1. Pattern Matching for instanceof
Before Pattern Matching (The Old Way):
public void printLength(Object obj) {
if (obj instanceof String) {
String s = (String) obj; // Boilerplate cast
System.out.println(s.length());
}
}
After Pattern Matching (Java 16+):
public void printLength(Object obj) {
if (obj instanceof String s) {
// 's' is automatically cast and bound
System.out.println(s.length());
}
// 's' is out of scope here
}
2. Flow Scoping and Short-Circuiting
You can use pattern variables in the same line they are declared, provided the logic short-circuits appropriately.
public boolean isLongString(Object obj) {
// 's' is only evaluated in the second half of the && if the first half is true
return (obj instanceof String s) && s.length() > 50;
}
3. Pattern Matching for switch (Java 21)
Switch statements can now route logic based on the type of the object, acting as a powerful type-based dispatcher.
public String formatObject(Object obj) {
return switch (obj) {
case Integer i -> "An integer: " + i;
case String s -> "A string of length " + s.length();
case null -> "Null object!"; // Nulls can now be safely handled
default -> "Unknown object type";
};
}
4. Record Patterns (Deconstruction)
If you are using Java Records, you can deconstruct them directly into their component parts.
record Point(int x, int y) {}
record Line(Point start, Point end) {}
public void analyzeGeometry(Object shape) {
if (shape instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
// Deep extraction: x1, y1, x2, y2 are instantly available!
System.out.printf("Line goes from (%d,%d) to (%d,%d)%n", x1, y1, x2, y2);
}
}
Common Pitfalls and Best Practices
1. Shadowing Variables
The Mistake: Using pattern variable names that shadow fields in the class. While legal, it heavily degrades readability.
The Fix: Use descriptive variable names or standard naming conventions (e.g., case Employee emp) to avoid confusion.
2. Missing Exhaustiveness in switch
The Rule: When using pattern matching in a switch expression (where a value is returned), the switch must be exhaustive. This means it must handle every possible type the target object could be.
The Fix: Always include a default case, or if matching against a sealed interface, ensure all permitted implementations are covered as cases. The compiler will enforce this.
3. Guard Clauses (When Clauses)
Sometimes a type match isn't enough; you need additional conditional logic. Use the when keyword (introduced in Java 21) to add guards to switch cases.
case String s when s.length() > 100 -> "Large String";
case String s -> "Normal String";
Interview Q&A
Q1: What happens if the object being tested in instanceof is null?
Answer: If the target object is null, obj instanceof Type t safely evaluates to false. The pattern variable t is never bound, preventing a NullPointerException.
Q2: How does Flow Scoping work with the ! (NOT) operator?
Answer: Flow scoping acts exactly as you'd logically expect. If you write if (!(obj instanceof String s)) { return; }, the variable s is actually in scope after the if-statement block, because the code will only reach that point if the object is a String.
Q3: Can pattern matching in a switch handle null values?
Answer: Yes, starting in Java 21, you can explicitly add case null -> to a switch statement. In older Java versions, passing null to a switch threw a NullPointerException.