AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

19 Modern Java / Lesson 3 of 6

Pattern matching — instanceof, switch and record patterns

15 min · +10 XP · 2 exercises · 6 questions

Read the text15 min
Exercises0/2
Quiz6 q

Quick answer

What is pattern matching in Java?

Pattern matching in Java tests a value's shape and binds its parts in one step. if (o instanceof String s) checks the type and gives a typed variable s without a cast (final in Java 16). Java 21 extends it to switch, whose cases can be type patterns and record patterns with when guards, and the compiler checks that a switch over a sealed type covers every subtype.

Pattern matching is the biggest change to how Java code reads since lambdas. It began quietly in Java 16 — if (o instanceof String s) binds s without a cast — and by Java 21 it lets a switch take a value of a sealed type apart by shape, down through nested records, with the compiler checking that every case is covered. The result is that the "sum type + function by cases" style of functional languages is now idiomatic Java, and the visitor pattern is mostly retired. This lesson covers all three pieces, what compiles on the runtime you have (Java 17-level: instanceof patterns, records, sealed types) and what needs 21 (pattern switch, record patterns), and the exhaustiveness rule that makes it safe.

Pattern instanceof (Java 16)

// before
if (o instanceof String) { String s = (String) o; use(s.length()); }
// after
if (o instanceof String s) { use(s.length()); }
if (o instanceof String s && s.length() > 3) { … }          // s is in scope on the right of &&
if (!(o instanceof String s)) return;                        // flow scoping: s is in scope AFTER this line
use(s);

The type pattern String s tests and binds in one step. Scope follows definite matching: s exists wherever the compiler can prove the test succeeded — the if body, the right of &&, and after an early return on the negated test. It does not exist on the right of ||, and a pattern variable may shadow a field but not a local. The cast is gone, and with it the ClassCastException you get when the check and the cast drift apart during a refactor.

Sealed types set up the switch

Recall from the interfaces module: a sealed interface Shape permits Circle, Square, Rect fixes the set of implementations. That fixed set is what lets a switch over it be exhaustive without a default. Until pattern switch arrived, the way to dispatch on a sealed hierarchy was a chain of pattern instanceofs — still the form you write on Java 17:

sealed interface Shape permits Circle, Square, Rect {}
record Circle(double r) implements Shape {}
record Square(double side) implements Shape {}
record Rect(double w, double h) implements Shape {}

static double area(Shape s) {
    if (s instanceof Circle c) return Math.PI * c.r() * c.r();
    if (s instanceof Square q) return q.side() * q.side();
    if (s instanceof Rect r) return r.w() * r.h();
    throw new IllegalStateException("unreachable: " + s);      // the compiler cannot see exhaustiveness in an if-chain
}

Pattern matching for switch (Java 21)

static double area(Shape s) {
    return switch (s) {
        case Circle c -> Math.PI * c.r() * c.r();
        case Square q -> q.side() * q.side();
        case Rect r   -> r.w() * r.h();
    };                                            // no default: the compiler proves every permitted subtype is covered
}

What changed: case may be a type pattern; the selector may be any reference type (not just primitives, strings and enums); the switch is checked for exhaustiveness — for a sealed type it must cover every permitted subtype (or have a default); and cases are checked for dominance — a case Object o before case String s is a compile error because it would swallow it. Add a subtype to Shape and every exhaustive switch in the codebase fails to compile until it handles the new case. That is the safety the visitor pattern promised and delivered only with boilerplate.

Guards refine a case with when:

case Circle c when c.r() == 0 -> 0;
case Circle c -> Math.PI * c.r() * c.r();

null can be a case — case null -> … — or case null, default ->; a switch without it throws NullPointerException on a null selector, as switches always have.

Record patterns (Java 21)

A record pattern deconstructs a record into its components, and nests:

record Point(int x, int y) {}
record Line(Point from, Point to) {}

if (o instanceof Point(int x, int y)) System.out.println(x + y);
static String describe(Object o) {
    return switch (o) {
        case Line(Point(var x1, var y1), Point(var x2, var y2)) when x1 == x2 -> "vertical";
        case Line(Point from, Point to) -> "line";
        case Point(int x, int y) when x == 0 && y == 0 -> "origin";
        case Point p -> "point";
        default -> "other";
    };
}

var is allowed for component types. Nesting goes as deep as the data does, and the compiler checks that the components exist and are compatible. Java 22 adds _ for components you do not need (case Point(int x, _)). Records + sealed interfaces + pattern switch is the trio: algebraic data types with exhaustive matching, and it is how modern Java models results (Ok/Err), commands, AST nodes and states.

Where this replaces old idioms

OldNew
instanceof + castpattern instanceof
visitor pattern over a hierarchysealed + exhaustive pattern switch
chain of if (x instanceof …)pattern switch with dominance and exhaustiveness checks
equals-based switch on a "type" string fieldswitch on the actual type
getters to unpack a data objectrecord pattern

The visitor is not dead — it still allows adding operations without touching the hierarchy's definition module — but for code that owns both the types and the operations, patterns win on every axis.

Compiler guarantees, summarised

  • Exhaustiveness: a switch expression must cover all cases; over a sealed type, every permitted subtype (recursively for sealed subtypes); over an enum, every constant; otherwise a default is required. A switch statement with patterns must also be exhaustive (older statement switches on enums need not be).
  • Dominance: an earlier case may not dominate a later one; unguarded type patterns come after guarded ones for the same type.
  • Scope: pattern variables are definitely assigned only where the match is proven.
  • Separate compilation: if a sealed hierarchy changes and a switch is not recompiled, the JVM throws MatchException at run time — the compile-time promise degrades gracefully.

Interview angle

  • "What does if (o instanceof String s) do?" Tests and binds in one; s is scoped where the match is definite; no cast.
  • "What does sealed buy you with switch?" Exhaustiveness without default: adding a subtype breaks every switch that forgets it.
  • "Pattern switch versus visitor?" Same exhaustiveness guarantee, no boilerplate, but operations live with the types; visitor still allows extension across modules.
  • "What is a record pattern?" Deconstructing a record into components in a case or instanceof, nestable.
  • "What happens on switch (null)?" NullPointerException unless there is a case null.

Key takeaways

  • Pattern instanceof (16): test + bind, flow-scoped, no cast; on Java 17 an if-chain is how you dispatch over a sealed type.
  • Pattern switch (21): type patterns as cases, when guards, case null, exhaustiveness and dominance checked by the compiler.
  • Record patterns (21) deconstruct, nest, and take var; sealed + records + switch = algebraic data types.
  • Exhaustive switches turn "I added a subtype" into compile errors at every site that must change.
  • Prefer patterns to visitors when you own both the hierarchy and the operations.

Common questions

How does instanceof pattern matching work in Java?

o instanceof String s tests whether o is a String and, if it is, binds it to s with that type, so no cast is needed. s is in scope wherever the match is certain: the if body, the right-hand side of &&, and after an early return on the negated test.

What is an exhaustive switch in Java?

A switch the compiler proves covers every possible value, so no default is needed. Over a sealed type it must handle every permitted subtype, and adding a new subtype turns every switch that forgets it into a compile error.

What is a record pattern in Java?

A record pattern deconstructs a record into its components inside instanceof or a case, as in case Point(int x, int y), and patterns can nest, such as a Line of two Points. Record patterns are final since Java 21, and var may stand for a component type.

What happens when you switch on null in Java?

The switch throws NullPointerException, as switches always have, unless it has a case null (or case null, default) that handles the null selector explicitly.

Does pattern matching replace the visitor pattern in Java?

Mostly, when you own both the types and the operations: a sealed hierarchy with an exhaustive pattern switch gives the same guarantee without the boilerplate. The visitor still helps when new operations must be added in another module without touching the hierarchy's definition.

Which Java version added pattern matching for switch?

Java 21 made pattern matching for switch and record patterns final, after previews that began in Java 17. Pattern matching for instanceof was final earlier, in Java 16.

Mark as readSign in to keep your place.

Java Pattern Matching: instanceof, switch and Record Patterns

Pattern matching in Java: instanceof binds a typed variable without a cast, and Java 21 switch matches types and record patterns with exhaustiveness checks.

  • Course: Java study plan
  • Module: Modern Java
  • Kind: Lesson
  • Reading time: 15 min
  • Runtime: OpenJDK 18

What is pattern matching in Java?

Pattern matching in Java tests a value's shape and binds its parts in one step. if (o instanceof String s) checks the type and gives a typed variable s without a cast (final in Java 16). Java 21 extends it to switch, whose cases can be type patterns and record patterns with when guards, and the compiler checks that a switch over a sealed type covers every subtype.

Lesson

Pattern matching is the biggest change to how Java code reads since lambdas. It began quietly in Java 16 — if (o instanceof String s) binds s without a cast — and by Java 21 it lets a switch take a value of a sealed type apart by shape, down through nested records, with the compiler checking that every case is covered. The result is that the "sum type + function by cases" style of functional languages is now idiomatic Java, and the visitor pattern is mostly retired. This lesson covers all three pieces, what compiles on the runtime you have (Java 17-level: instanceof patterns, records, sealed types) and what needs 21 (pattern switch, record patterns), and the exhaustiveness rule that makes it safe.

Pattern instanceof (Java 16)

// before
if (o instanceof String) { String s = (String) o; use(s.length()); }
// after
if (o instanceof String s) { use(s.length()); }
if (o instanceof String s && s.length() > 3) { … }          // s is in scope on the right of &&
if (!(o instanceof String s)) return;                        // flow scoping: s is in scope AFTER this line
use(s);

The type pattern String s tests and binds in one step. Scope follows definite matching: s exists wherever the compiler can prove the test succeeded — the if body, the right of &&, and after an early return on the negated test. It does not exist on the right of ||, and a pattern variable may shadow a field but not a local. The cast is gone, and with it the ClassCastException you get when the check and the cast drift apart during a refactor.

Sealed types set up the switch

Recall from the interfaces module: a sealed interface Shape permits Circle, Square, Rect fixes the set of implementations. That fixed set is what lets a switch over it be exhaustive without a default. Until pattern switch arrived, the way to dispatch on a sealed hierarchy was a chain of pattern instanceofs — still the form you write on Java 17:

sealed interface Shape permits Circle, Square, Rect {}
record Circle(double r) implements Shape {}
record Square(double side) implements Shape {}
record Rect(double w, double h) implements Shape {}

static double area(Shape s) {
    if (s instanceof Circle c) return Math.PI * c.r() * c.r();
    if (s instanceof Square q) return q.side() * q.side();
    if (s instanceof Rect r) return r.w() * r.h();
    throw new IllegalStateException("unreachable: " + s);      // the compiler cannot see exhaustiveness in an if-chain
}

Pattern matching for switch (Java 21)

static double area(Shape s) {
    return switch (s) {
        case Circle c -> Math.PI * c.r() * c.r();
        case Square q -> q.side() * q.side();
        case Rect r   -> r.w() * r.h();
    };                                            // no default: the compiler proves every permitted subtype is covered
}

What changed: case may be a type pattern; the selector may be any reference type (not just primitives, strings and enums); the switch is checked for exhaustiveness — for a sealed type it must cover every permitted subtype (or have a default); and cases are checked for dominance — a case Object o before case String s is a compile error because it would swallow it. Add a subtype to Shape and every exhaustive switch in the codebase fails to compile until it handles the new case. That is the safety the visitor pattern promised and delivered only with boilerplate.

Guards refine a case with when:

case Circle c when c.r() == 0 -> 0;
case Circle c -> Math.PI * c.r() * c.r();

null can be a case — case null -> … — or case null, default ->; a switch without it throws NullPointerException on a null selector, as switches always have.

Record patterns (Java 21)

A record pattern deconstructs a record into its components, and nests:

record Point(int x, int y) {}
record Line(Point from, Point to) {}

if (o instanceof Point(int x, int y)) System.out.println(x + y);
static String describe(Object o) {
    return switch (o) {
        case Line(Point(var x1, var y1), Point(var x2, var y2)) when x1 == x2 -> "vertical";
        case Line(Point from, Point to) -> "line";
        case Point(int x, int y) when x == 0 && y == 0 -> "origin";
        case Point p -> "point";
        default -> "other";
    };
}

var is allowed for component types. Nesting goes as deep as the data does, and the compiler checks that the components exist and are compatible. Java 22 adds _ for components you do not need (case Point(int x, _)). Records + sealed interfaces + pattern switch is the trio: algebraic data types with exhaustive matching, and it is how modern Java models results (Ok/Err), commands, AST nodes and states.

Where this replaces old idioms

OldNew
instanceof + castpattern instanceof
visitor pattern over a hierarchysealed + exhaustive pattern switch
chain of if (x instanceof …)pattern switch with dominance and exhaustiveness checks
equals-based switch on a "type" string fieldswitch on the actual type
getters to unpack a data objectrecord pattern

The visitor is not dead — it still allows adding operations without touching the hierarchy's definition module — but for code that owns both the types and the operations, patterns win on every axis.

Compiler guarantees, summarised

  • Exhaustiveness: a switch expression must cover all cases; over a sealed type, every permitted subtype (recursively for sealed subtypes); over an enum, every constant; otherwise a default is required. A switch statement with patterns must also be exhaustive (older statement switches on enums need not be).
  • Dominance: an earlier case may not dominate a later one; unguarded type patterns come after guarded ones for the same type.
  • Scope: pattern variables are definitely assigned only where the match is proven.
  • Separate compilation: if a sealed hierarchy changes and a switch is not recompiled, the JVM throws MatchException at run time — the compile-time promise degrades gracefully.

Interview angle

  • "What does if (o instanceof String s) do?" Tests and binds in one; s is scoped where the match is definite; no cast.
  • "What does sealed buy you with switch?" Exhaustiveness without default: adding a subtype breaks every switch that forgets it.
  • "Pattern switch versus visitor?" Same exhaustiveness guarantee, no boilerplate, but operations live with the types; visitor still allows extension across modules.
  • "What is a record pattern?" Deconstructing a record into components in a case or instanceof, nestable.
  • "What happens on switch (null)?" NullPointerException unless there is a case null.

Key takeaways

  • Pattern instanceof (16): test + bind, flow-scoped, no cast; on Java 17 an if-chain is how you dispatch over a sealed type.
  • Pattern switch (21): type patterns as cases, when guards, case null, exhaustiveness and dominance checked by the compiler.
  • Record patterns (21) deconstruct, nest, and take var; sealed + records + switch = algebraic data types.
  • Exhaustive switches turn "I added a subtype" into compile errors at every site that must change.
  • Prefer patterns to visitors when you own both the hierarchy and the operations.

Common questions

How does instanceof pattern matching work in Java?

o instanceof String s tests whether o is a String and, if it is, binds it to s with that type, so no cast is needed. s is in scope wherever the match is certain: the if body, the right-hand side of &&, and after an early return on the negated test.

What is an exhaustive switch in Java?

A switch the compiler proves covers every possible value, so no default is needed. Over a sealed type it must handle every permitted subtype, and adding a new subtype turns every switch that forgets it into a compile error.

What is a record pattern in Java?

A record pattern deconstructs a record into its components inside instanceof or a case, as in case Point(int x, int y), and patterns can nest, such as a Line of two Points. Record patterns are final since Java 21, and var may stand for a component type.

What happens when you switch on null in Java?

The switch throws NullPointerException, as switches always have, unless it has a case null (or case null, default) that handles the null selector explicitly.

Does pattern matching replace the visitor pattern in Java?

Mostly, when you own both the types and the operations: a sealed hierarchy with an exhaustive pattern switch gives the same guarantee without the boilerplate. The visitor still helps when new operations must be added in another module without touching the hierarchy's definition.

Which Java version added pattern matching for switch?

Java 21 made pattern matching for switch and record patterns final, after previews that began in Java 17. Pattern matching for instanceof was final earlier, in Java 16.

Exercises

Shapes with pattern instanceof

Declare sealed interface Shape permits Circle, Square, Rect with records Circle(double r), Square(double side) and Rect(double w, double h). Read an integer n and n shapes — circle <r>, square <side> or rect <w> <h> — and for each print <Kind> area=<two decimals> perimeter=<two decimals> using pattern instanceof (no casts). Then print largest=<record toString of the largest-area shape> and total=<sum of areas, two decimals>. Use Locale.ROOT.

Example: 2 then circle 1, rect 2 3 →

Circle area=3.14 perimeter=6.28
Rect area=6.00 perimeter=10.00
largest=Rect[w=2.0, h=3.0]
total=9.14

An expression tree

Declare sealed interface Expr permits Num, Add, Mul, Neg with records Num(int value), Add(Expr left, Expr right), Mul(Expr left, Expr right) and Neg(Expr inner). Read one line of reverse Polish tokens — integers, +, *, and ~ for negation — and build the tree with a stack. Then, with pattern instanceof, print expr=<rendering> where a Num is its value, Add is (l + r), Mul is (l * r) and Neg is -(x); value=<evaluation>; and depth=<height of the tree, a lone Num being 1>.

Example: 1 2 3 * + →

expr=(1 + (2 * 3))
value=7
depth=3

In this module: Modern Java

← var and type inference — what the compiler already knows · The API additions worth knowing, 9 to 21 →