Pattern Matching
Pattern matching lets you test whether a value has a certain shape or type, and — if it matches — immediately bind part of it to a new variable, all in one step. Java has added pattern matching incrementally: first to instanceof (Java 16), then to switch (Java 21), and it dramatically cuts down the defensive cast-after-check boilerplate that used to be everywhere in Java code.
Pattern matching for instanceof
Before Java 16, checking a type with instanceof and then using the value as that type required a separate, explicit cast right after the check — the compiler already knew the type was safe to cast, but you still had to write the cast yourself.
Before: check, then cast
Object obj = "hello";
if (obj instanceof String) {
String s = (String) obj; // redundant cast — we already checked!
System.out.println(s.toUpperCase());
}Pattern matching for instanceof lets you name the cast variable right in the instanceof expression. If the check succeeds, that variable is automatically populated with the value, already cast — and it's only in scope where the compiler can prove the check passed.
After: instanceof binds the variable directly
Object obj = "hello";
if (obj instanceof String s) {
System.out.println(s.toUpperCase()); // s is already a String, no cast
}
// The pattern variable's scope can extend past the if when the compiler
// can prove it must have matched — handy for early-return guard clauses
void process(Object obj) {
if (!(obj instanceof String s)) {
return;
}
System.out.println(s.toUpperCase()); // s is in scope here too
}Pattern matching for switch
Java 21 extends pattern matching into switch, so a switch expression or statement can match directly on an object’s runtime type — each case acts like a type-checked, auto-cast branch, replacing long if / else if (x instanceof Y) chains.
Before: an if/else if chain of instanceof checks
String describe(Object obj) {
if (obj instanceof Integer i) {
return "An integer: " + i;
} else if (obj instanceof String s) {
return "A string of length " + s.length();
} else if (obj == null) {
return "It's null";
} else {
return "Something else: " + obj;
}
}After: switch pattern matching (Java 21)
String describe(Object obj) {
return switch (obj) {
case Integer i -> "An integer: " + i;
case String s -> "A string of length " + s.length();
case null -> "It's null";
default -> "Something else: " + obj;
};
}Notice switch pattern matching can even match null directly as a case, something a classic switch never allowed (it used to throw a NullPointerException on a null subject).
Record patterns (deconstruction)
Pattern matching goes a step further with record patterns: if the type being matched is a record, you can deconstruct it right in the pattern, binding variables to its components directly instead of matching the whole record and then calling accessor methods.
Deconstructing a record in a switch pattern
record Point(int x, int y) {}
String classify(Object obj) {
return switch (obj) {
case Point(int x, int y) when x == 0 && y == 0 -> "origin";
case Point(int x, int y) when x == y -> "on the diagonal";
case Point(int x, int y) -> "point at (" + x + ", " + y + ")";
default -> "not a point";
};
}The when clause adds a further boolean guard on top of the matched pattern, letting you express conditions that go beyond just type and shape. Record patterns can even nest, deconstructing a record whose component is itself another record. (See the Records page for more on what makes a type a record in the first place.)
Why this matters: exhaustiveness
Pattern matching in switch pairs naturally with sealed classes, because the compiler knows the complete, closed set of subtypes a sealed hierarchy permits. When every possible subtype has a case, the compiler can verify the switch is exhaustive and skip requiring a default branch — turning what used to be a runtime risk (an unhandled subtype silently falling through) into a compile-time guarantee. (See the Sealed Classes page for the hierarchy-restriction side of this story.)
instanceofpattern matching (Java 16+) binds a cast variable directly in the check:if (obj instanceof String s).Pattern matching for
switch(Java 21) matchescaselabels against runtime types, includingcase null.Record patterns deconstruct a record's components directly in a pattern, optionally guarded by a
whenclause.Pattern matching combined with sealed classes lets the compiler verify a
switchhandles every possible subtype.