Pattern Matching
Pattern matching, delivered across several JEPs from Java 16 through Java 21 — pattern matching for instanceof (Java 16), records (Java 16, used heavily by record patterns), pattern matching for switch (finalized Java 21), and record patterns (finalized Java 21) — lets a single instanceof check or switch case simultaneously test a value's type and bind it to a correctly-typed local variable, and (with record patterns) recursively destructure record components inline, eliminating the previously mandatory separate explicit cast that always followed a successful instanceof check. This entry covers exactly what redundancy pattern matching for instanceof removes, pattern matching for switch including guarded patterns and null handling, record patterns and nested destructuring, and how exhaustiveness checking extends naturally to sealed type hierarchies under switch pattern matching.
Pattern Matching for instanceof — Removing the Mandatory Redundant Cast
// ── Before Java 16 — instanceof followed by a mandatory, redundant cast ─
Object obj = "hello";
if (obj instanceof String) {
String s = (String) obj; // redundant: type already confirmed by instanceof above,
System.out.println(s.length()); // but the cast must still be written out explicitly
}
// ── Java 16+ — pattern variable bound directly in the instanceof itself ─
if (obj instanceof String s) {
System.out.println(s.length()); // "s" is already a String — no separate cast at all
}
// ── Pattern variable scope extends past early-exit negated checks ──────
void process(Object obj) {
if (!(obj instanceof String s)) {
return; // if this check is false (obj IS a String), we return here instead
}
// Reaching this point PROVES obj instanceof String was true — "s" is usable here,
// even though it was declared inside a NEGATED instanceof condition:
System.out.println(s.toUpperCase());
}
// ── Combining the pattern check directly into a boolean expression ─────
Object value = 42;
if (value instanceof Integer i && i > 10) { // pattern variable usable in the SAME condition
System.out.println("Big integer: " + i);
}Pattern Matching for switch — Guarded Patterns and Null Handling
// ── Before — chained instanceof checks expressing type dispatch ────────
String describeOld(Object obj) {
if (obj instanceof Integer i) {
return "Integer: " + i;
} else if (obj instanceof String s) {
return "String of length " + s.length();
} else if (obj instanceof Double d) {
return "Double: " + d;
} else {
return "Unknown: " + obj;
}
}
// ── Java 21 — pattern matching for switch, same logic, reads as dispatch
String describe(Object obj) {
return switch (obj) {
case Integer i -> "Integer: " + i;
case String s -> "String of length " + s.length();
case Double d -> "Double: " + d;
default -> "Unknown: " + obj;
};
}
// ── Guarded patterns — "when" adds a further condition beyond the type ─
String classify(Object obj) {
return switch (obj) {
case Integer i when i > 0 -> "Positive integer";
case Integer i when i < 0 -> "Negative integer";
case Integer i -> "Zero"; // falls through here if neither guard matched
case String s when s.isBlank() -> "Blank string";
case String s -> "Non-blank string: " + s;
default -> "Other: " + obj;
};
}
// ── Explicit "case null" — null handled directly within the switch ─────
String describeWithNull(Object obj) {
return switch (obj) {
case null -> "It's null!"; // handled explicitly, no separate pre-check needed
case Integer i -> "Integer: " + i;
default -> "Non-null: " + obj;
};
}
// Without an explicit "case null" label, a null selector still throws NPE exactly
// as a traditional switch always has — behavior is unchanged unless opted intoRecord Patterns — Nested Destructuring and Sealed-Type Exhaustiveness
// ── Basic record pattern — destructure components directly in the match
record Point(int x, int y) {}
void printCoordinates(Object obj) {
if (obj instanceof Point(int x, int y)) { // destructures BOTH components at once
System.out.println("x=" + x + ", y=" + y); // no point.x()/point.y() calls needed
}
}
// ── Nested record patterns — recursive destructuring of nested records ─
record Location(String city, Point coordinates) {}
record Event(String name, Location location) {}
void describeEvent(Object obj) {
if (obj instanceof Event(String name, Location(String city, Point(int x, int y)))) {
// Fully destructured THROUGH two levels of nesting in one pattern:
System.out.println(name + " in " + city + " at (" + x + "," + y + ")");
}
}
// ── var can be used for individual pattern components ───────────────────
if (obj instanceof Point(var x, var y)) { // inferred as int, int — still fully type-checked
System.out.println(x + y);
}
// ── Record patterns + sealed types + switch — exhaustive AND destructuring
sealed interface Shape permits Circle, Rectangle {}
record Circle(Point center, double radius) implements Shape {}
record Rectangle(Point topLeft, Point bottomRight) implements Shape {}
double area(Shape shape) {
return switch (shape) {
case Circle(Point(var cx, var cy), var radius) ->
Math.PI * radius * radius; // destructured center coords + radius directly
case Rectangle(Point(var x1, var y1), Point(var x2, var y2)) ->
Math.abs((x2 - x1) * (y2 - y1)); // both corner points destructured in one pattern
// no default needed — Circle/Rectangle are Shape's complete permitted set,
// compiler-verified exhaustive exactly as with non-destructuring sealed switches
};
}