Record Patterns Interview Questions & Answers
Java record patterns interview questions — deconstruction patterns, nested patterns, record patterns in instanceof, record patterns in switch, combining with sealed classes, var in patterns, and replacing instanceof chains.
Record patterns (Java 21, JEP 440) extend pattern matching to
destructure record components directly in an instanceof or switch
expression, binding each component to a local variable in one step.
record Point(int x, int y) {}
Object obj = new Point(3, 4);
// Old way — instanceof check, then accessor calls:
if (obj instanceof Point p) {
int x = p.x();
int y = p.y();
System.out.println(x + ", " + y);
}
// Record pattern — destructure inline:
if (obj instanceof Point(int x, int y)) {
System.out.println(x + ", " + y); // x and y bound directly
}
Rule of thumb: use record patterns when you need multiple components of a record — they eliminate the accessor boilerplate.
Record patterns integrate seamlessly into switch type patterns, making multi-branch dispatch over record-carrying sealed types concise:
sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
double area(Shape s) {
return switch (s) {
case Circle(double r) -> Math.PI * r * r;
case Rectangle(double w, double h) -> w * h;
};
}
The components r, w, h are bound directly in each case — no
accessor calls, no intermediate variables.
Rule of thumb: record patterns in switch are the natural companion to sealed interfaces — together they model exhaustive dispatch over typed data cleanly.
Record pattern components can themselves be patterns — you can destructure a component that is another record, arbitrarily deep:
record Point(int x, int y) {}
record Line(Point start, Point end) {}
Object obj = new Line(new Point(0, 0), new Point(3, 4));
if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
double length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
System.out.println("Length: " + length); // 5.0
}
Nested patterns are especially powerful for recursive data structures like expression trees, JSON documents, or domain events.
Rule of thumb: nested patterns eliminate deep chains of accessor calls on nested records — write the shape of the data you expect, bind the values.
Yes. var infers the component type, which is useful when the type is
long or obvious from context:
record Pair<A, B>(A first, B second) {}
Object obj = new Pair<>("hello", 42);
if (obj instanceof Pair(var first, var second)) {
System.out.println(first + " " + second); // "hello 42"
// first is String, second is Integer — inferred
}
var in a record pattern behaves the same as var in a local variable
declaration — the type is fixed at compile time, just written shorter.
Rule of thumb: use var in record patterns when the component types
are verbose (e.g., Map.Entry<String, List<Integer>>) or clearly
apparent from context.
Record patterns support generic records, but due to type erasure the
component types must be compatible with the inferred erasure. Using
explicit types works; using concrete parameterisations may require a
cast or var:
record Box<T>(T value) {}
Object obj = new Box<>("hello");
// Using var — inferred as Object due to erasure:
if (obj instanceof Box(var v)) {
System.out.println(v); // "hello" — but v is typed as Object
}
// Explicit type — works if you know the component type:
if (obj instanceof Box(String s)) {
System.out.println(s.toUpperCase()); // s is String — unchecked
}
The compiler may emit an unchecked warning for the explicit-type case because the generic argument is not verifiable at runtime (erasure).
Rule of thumb: use var for generic record patterns to avoid
unchecked warnings; add an explicit type only when you're certain of
the component's runtime type.
Yes. when guards work with record patterns just like with type patterns:
sealed interface Expr permits Num, Add {}
record Num(int v) implements Expr {}
record Add(Expr l, Expr r) implements Expr {}
String describe(Expr e) {
return switch (e) {
case Num(int v) when v < 0 -> "negative: " + v;
case Num(int v) when v == 0 -> "zero";
case Num(int v) -> "positive: " + v;
case Add(Expr l, Expr r) -> "sum of " + describe(l) + " and " + describe(r);
};
}
Rule of thumb: combine when with record patterns to filter on
component values without a nested if inside the case body.
The Visitor pattern was a workaround for Java's lack of multi-dispatch and sealed type exhaustiveness. Record patterns + sealed interfaces + switch replace it with far less ceremony:
// Visitor pattern — 30+ lines, double dispatch, accept/visit boilerplate:
interface ExprVisitor<R> {
R visitNum(Num n);
R visitAdd(Add a);
}
// Each record implements accept(ExprVisitor<R>)...
// Record patterns — same result, no infrastructure:
sealed interface Expr permits Num, Add {}
record Num(int v) implements Expr {}
record Add(Expr l, Expr r) implements Expr {}
int eval(Expr e) {
return switch (e) {
case Num(int v) -> v;
case Add(Expr l, Expr r) -> eval(l) + eval(r);
};
}
String print(Expr e) {
return switch (e) {
case Num(int v) -> String.valueOf(v);
case Add(Expr l, Expr r) -> "(" + print(l) + " + " + print(r) + ")";
};
}
Adding a new operation (eval, print, typecheck) requires writing one
new function — no changes to existing classes, no new visitor interfaces.
Rule of thumb: sealed + records + switch patterns = Visitor pattern without the boilerplate. Adding a new type forces you to update every switch (compile error); adding a new operation requires only a new function.
Variables bound in a record pattern instanceof follow the same
flow-sensitive scoping rules as simple type patterns — they are in scope
only in the branch where the match is guaranteed:
record Point(int x, int y) {}
Object obj = new Point(1, 2);
if (obj instanceof Point(int x, int y)) {
System.out.println(x + ", " + y); // in scope here
}
// x and y are NOT in scope here
// Also works with && guards:
if (obj instanceof Point(int x, int y) && x > 0 && y > 0) {
System.out.println("first quadrant: " + x + ", " + y);
}
Rule of thumb: record pattern variables behave exactly like simple
pattern variables — scoped to the true branch and && chains.
Like all instanceof patterns, a record pattern does not match null —
instanceof returns false for null regardless of the pattern:
record Point(int x, int y) {}
Object obj = null;
System.out.println(obj instanceof Point(int x, int y)); // false
// x and y are never bound; no NPE
// In switch, null must be handled explicitly:
switch (obj) {
case null -> System.out.println("null");
case Point(int x, int y) -> System.out.println(x + "," + y);
default -> System.out.println("other");
}
Rule of thumb: null never matches a record pattern — handle it with
case null in switch or an explicit null check before instanceof.
Exhaustiveness checking applies to the outer type pattern, not to the internal component structure. When the selector is a sealed type, the compiler checks that all permitted subtypes are covered:
sealed interface Shape permits Circle, Rect {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}
// Exhaustive — all subtypes covered:
double area(Shape s) {
return switch (s) {
case Circle(double r) -> Math.PI * r * r;
case Rect(double w, double h) -> w * h;
};
}
// Adding a new Shape subtype → compile error here
The compiler does not (currently) check exhaustiveness of component
values — that's the role of when guards plus a default or catch-all.
Rule of thumb: exhaustiveness in switch refers to which subtypes are
covered, not which component values — add when guards for component-level
conditions and a final ungarded case as the catch-all.
Use a record pattern when you need most or all components of a record:
// If you need both x and y — record pattern is cleaner:
if (obj instanceof Point(int x, int y)) {
System.out.println(x + ", " + y);
}
// If you need only one component — accessor is simpler:
if (obj instanceof Point p) {
System.out.println(p.x()); // only x needed
}
For nested records where you'd need multiple accessor calls, the pattern is significantly more readable:
// Accessor chain — verbose:
if (obj instanceof Line l) {
int x1 = l.start().x();
int y1 = l.start().y();
int x2 = l.end().x();
int y2 = l.end().y();
...
}
// Nested record pattern — concise:
if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
...
}
Rule of thumb: record patterns pay off when you need multiple components or when nesting is deep; stick to accessors for single-field access.
More Modern Java interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.