Java Pattern Matching
Use pattern matching in Java for instanceof and switch — type patterns, record patterns, and deconstruction.
Java Pattern Matching
For years, Java code that worked with values of unknown type followed a tedious ritual: test the type with instanceof, then cast to that type, then use it. Pattern matching collapses that ritual into a single expression. A pattern describes the shape of data; if a value matches, Java binds its parts to variables you can use right away — no manual cast required.
Pattern matching arrived in stages: instanceof patterns first, then patterns in switch, then record patterns that deconstruct records into their components. Together they let you write declarative, type-safe code that reads like the data it operates on.
Pattern Matching for instanceof
The classic test-and-cast pattern needed three references to the same type. The instanceof pattern binds a variable in the same breath as the test, and the binding is in scope wherever the test is known to be true.
Object value = "hello";
// Old way: test, then cast
if (value instanceof String) {
String s = (String) value;
System.out.println(s.length());
}
// Pattern way: test and bind together
if (value instanceof String s) {
System.out.println(s.length());
}Because the binding variable participates in the boolean expression, you can keep narrowing in the same if. The compiler proves s is safe to use:
if (value instanceof String s && s.length() > 3) {
System.out.println(s.toUpperCase());
}Patterns in switch
A switch can match on type patterns, dispatching by the runtime type of the selector. Each case binds the matched value, so the body works with a typed variable directly. This turns long if/else instanceof chains into a compact, readable table.
static String format(Object value) {
return switch (value) {
case Integer i -> "int: " + i;
case Long l -> "long: " + l;
case String s -> "string: " + s;
default -> "other: " + value;
};
}A type-patterned switch must be exhaustive — it has to cover every possible input. For arbitrary Object selectors that means a default branch; for sealed hierarchies the compiler knows the complete set of subtypes and can verify exhaustiveness without a default.
Guarded Patterns and null
A when clause adds a boolean condition to a case, letting two values of the same type take different branches. This is called a guarded pattern, and order matters: more specific guarded cases come before the unguarded fallback.
static String size(String s) {
return switch (s) {
case String t when t.isEmpty() -> "empty";
case String t when t.length() < 5 -> "short";
case String t -> "long (" + t.length() + ")";
};
}Traditionally a switch threw NullPointerException on a null selector. A patterned switch can handle null explicitly with a case null, keeping the null check inside the same construct instead of a separate guard before it.
| Feature | Syntax | Purpose |
|---|---|---|
| Type pattern | case String s | Match by type and bind |
| Guarded pattern | case String s when s.isEmpty() | Add a condition to a case |
| Null label | case null | Match a null selector |
| Record pattern | case Point(int x, int y) | Deconstruct a record |
Record Patterns
A record pattern matches a record and binds its components in one move, so you skip the accessor calls. Because records expose their components, the compiler knows the exact shape and lets you name each part inline. Record patterns nest, so you can destructure a record of records.
record Point(int x, int y) {}
record Line(Point start, Point end) {}
static String render(Object o) {
return switch (o) {
case Point(int x, int y) -> "point " + x + "," + y;
// Nested: pull both endpoints' coordinates out at once
case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
"line " + x1 + "," + y1 + " -> " + x2 + "," + y2;
default -> "unknown";
};
}Pattern matching shines with sealed types: when an interface lists its permitted implementations, a switch over them is exhaustive without a default, and adding a new subtype turns the missing case into a compile error rather than a silent bug.
A Complete, Runnable Example
The program below ties the pieces together. It uses an instanceof pattern with a guard, a sealed Shape hierarchy of records, record patterns that deconstruct each shape in a switch, a guarded pattern that spots a square, and a case null — all without a single explicit cast.
What to take from the run:
describe(42)printspositive int 42because theinstanceof Integer i && i > 0guard tests the type and the value together before bindingi.describe(-5)falls through tounknown— the sameIntegerpattern matches the type but thei > 0guard fails, showing how a guard refines a type pattern.- The
areaswitch needs nodefault:Shapeis sealed, so listingCircle,Rectangle, andTriangleis exhaustive and the compiler is satisfied. - The
5.0 x 5.0rectangle prints assquare side=5.0because its guardedwhen w == hcase is placed before the generalRectangle rcase and wins. - The final line prints
no shape: thecase nullbranch handles anullselector inside the switch instead of throwingNullPointerException.
Practice
In a patterned switch, what does adding a 'when' clause to a case do?