Switch Pattern Matching Interview Questions & Answers
Java switch pattern matching interview questions — switch expressions vs statements, type patterns, guarded patterns, record deconstruction, exhaustiveness, null handling in switch, arrow vs colon syntax, and pattern matching for instanceof.
A switch statement is an imperative control-flow construct that executes code for a matching case. A switch expression (Java 14, JEP 361) is an expression that produces a value and can be used anywhere an expression is expected.
// Switch statement — imperative, returns nothing
String msg;
switch (day) {
case MONDAY: msg = "start"; break;
default: msg = "other";
}
// Switch expression — yields a value
String msg = switch (day) {
case MONDAY -> "start";
default -> "other";
};
Switch expressions use arrow labels (->) which don't fall through
and don't need break. They must be exhaustive — every possible value
must be handled.
Rule of thumb: prefer switch expressions over switch statements; they're concise, don't fall through, and the compiler enforces exhaustiveness.
Arrow labels (case X ->) execute a single expression, block, or
throw and do not fall through to the next case.
Colon labels (case X:) are the traditional style — they fall through
to the next case unless a break or return stops them.
// Arrow — no fall-through, cleaner
int result = switch (x) {
case 1 -> 10;
case 2 -> 20;
default -> 0;
};
// Colon — falls through; needs break
switch (x) {
case 1:
case 2:
System.out.println("1 or 2"); // fall-through intentional here
break;
default:
System.out.println("other");
}
Arrow labels can also use a block with yield to produce a value:
int r = switch (x) {
case 1 -> 10;
default -> { int v = compute(x); yield v * 2; }
};
Rule of thumb: use arrow labels for new switch expressions; use colon labels only when intentional fall-through is needed.
yield returns a value from a block arm of a switch expression,
similar to return in a method but scoped to the switch:
int fee = switch (membership) {
case "gold" -> 0;
case "silver" -> 5;
default -> {
int base = 10;
int extra = membership.length();
yield base + extra; // produces the value of this arm
}
};
yield is only valid inside a switch expression block arm; in arrow
arms the expression result is yielded implicitly.
Rule of thumb: use yield only in block arms ({ ... }); single-
expression arms use the expression value automatically.
A type pattern (Java 21, JEP 441) lets a switch match on the
runtime type of the selector and bind the matched object to a variable in
one step — eliminating the cast:
Object obj = getObject();
String desc = switch (obj) {
case Integer i -> "int: " + i;
case String s -> "string: " + s;
case null -> "null";
default -> "other: " + obj.getClass().getSimpleName();
};
Compare the old way:
// Before type patterns:
if (obj instanceof Integer) {
Integer i = (Integer) obj; // explicit cast
...
}
Rule of thumb: type patterns in switch are cleaner than chains of
if/else instanceof + cast — and they're exhaustiveness-checked when
the selector is a sealed type.
A guarded pattern adds a boolean condition (when) to a type pattern
case, allowing fine-grained filtering without a nested if:
Object obj = getValue();
String result = switch (obj) {
case Integer i when i < 0 -> "negative int: " + i;
case Integer i when i == 0 -> "zero";
case Integer i -> "positive int: " + i;
case String s when s.isEmpty() -> "empty string";
case String s -> "string: " + s;
default -> "other";
};
The when clause is evaluated only if the type pattern matches. Cases
are evaluated top-to-bottom; more specific guards should come first.
Rule of thumb: use when guards to avoid nested if inside a case
block; order from most to least specific so the right case is selected first.
When the switch selector is a sealed type, the compiler verifies that every permitted subtype is covered by at least one case. If you add a new permitted subtype without updating the switch, you get a compile error:
sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double s) implements Shape {}
double area(Shape shape) {
return switch (shape) { // exhaustive — compiler verified
case Circle c -> Math.PI * c.r() * c.r();
case Square s -> s.s() * s.s();
};
// If we add 'Triangle' to Shape's permits, this becomes a compile error
}
For non-sealed types or Object, a default case (or a case null, default) is required to make the switch exhaustive.
Rule of thumb: sealed type + switch = compiler-enforced exhaustiveness;
default is your escape hatch for open hierarchies.
Traditionally, passing null to a switch statement throws a
NullPointerException. Java 21 allows explicit case null to handle it:
String s = getString(); // might be null
switch (s) {
case null -> System.out.println("null!");
case "hello" -> System.out.println("hello");
default -> System.out.println("other: " + s);
}
You can also combine null with default: case null, default ->.
If no case null is present, null still throws NullPointerException
to preserve backward compatibility.
Rule of thumb: add case null explicitly in pattern-matching switches
wherever null is a possible input; don't rely on NPE as flow control.
Record patterns (Java 21, JEP 440) destructure a record's components
directly in a case, binding them to variables without explicit accessor
calls:
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);
};
}
Nested deconstruction also works — you can deconstruct components that are themselves records:
record Point(int x, int y) {}
record Segment(Point start, Point end) {}
String describe(Segment seg) {
return switch (seg) {
case Segment(Point(int x1, int y1), Point(int x2, int y2))
-> "(%d,%d)→(%d,%d)".formatted(x1, y1, x2, y2);
};
}
Rule of thumb: record deconstruction removes the ceremony of
instanceof + cast + accessor chain — especially powerful for recursive
data types like ASTs.
Pattern matching for instanceof (Java 16, JEP 394) is the simpler
sibling: it binds a variable in a single instanceof check:
Object obj = getObject();
// Old way:
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.toUpperCase());
}
// Pattern matching (Java 16+):
if (obj instanceof String s) {
System.out.println(s.toUpperCase()); // s is in scope here
}
instanceof patterns also work in logical expressions:
if (obj instanceof String s && s.length() > 5) { ... }
Switch type patterns (Java 21) extend the same concept to multi-way dispatch. Both are part of the same pattern matching feature family.
Rule of thumb: use instanceof patterns for single-type checks;
use switch type patterns for multi-way dispatch over several types.
A type pattern case A a dominates case B b if every B is also
an A (i.e., B is a subtype of A). The compiler rejects a switch
where a more general pattern appears before a more specific one, because
the specific case would be unreachable:
Object obj = ...;
// Compile error — 'case Object o' dominates 'case String s':
switch (obj) {
case Object o -> System.out.println("object");
case String s -> System.out.println("string"); // unreachable!
}
// Correct — most specific first:
switch (obj) {
case String s -> System.out.println("string");
case Integer i -> System.out.println("int");
default -> System.out.println("other");
}
Rule of thumb: order cases from most specific to most general; the compiler will reject unreachable cases due to dominance.
Switch selectors have expanded across Java versions:
| Java version | Selector types |
|---|---|
| Traditional | byte, short, char, int, their wrappers, String, enums |
| Java 14 (switch expressions) | Same types, new syntax |
| Java 21 (pattern matching) | Any reference type (Object, interfaces, classes) |
// Java 21 — Object selector with type patterns:
Object val = getValue();
String desc = switch (val) {
case Integer i -> "int " + i;
case Long l -> "long " + l;
case String s -> "str " + s;
case null -> "null";
default -> "?" + val;
};
Note: primitive long, float, double are not yet valid selectors
(targeted for a future release via JEP 455 in Java 23 preview).
Rule of thumb: in Java 21+ you can switch on any object — the old
restriction to int/String/enum is lifted for pattern matching.
Both traditional and modern switch support multiple labels per case separated by commas:
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
};
This is cleaner than the old fall-through trick with colon syntax. In pattern-matching switches, combining type patterns with multiple labels is limited — each type pattern must be a separate case.
Rule of thumb: use comma-separated labels for grouping equivalent enum/constant cases; keep type pattern cases separate.
Prefer a switch expression when:
- You're selecting one of several branches based on a single value.
- The result is a value assigned to a variable or returned.
- You want the compiler to enforce exhaustiveness.
- The type is an enum, sealed type, or set of known constants.
Prefer if-else when:
- Conditions are complex boolean expressions (not just equality).
- You're testing different variables in each branch.
- Only one or two branches exist (switch adds noise for binary choices).
// Good switch expression — enum selector, value result, exhaustive
double discount = switch (tier) {
case GOLD -> 0.20;
case SILVER -> 0.10;
case BRONZE -> 0.05;
};
// Keep as if-else — unrelated conditions
if (user.isAdmin() && request.getSize() > MAX) { ... }
else if (ctx.isReadOnly()) { ... }
Rule of thumb: switch expressions shine on enums and sealed types; if-else is better for unrelated conditions or complex predicates.
This is the classic tell-don't-ask debate:
Polymorphism is better when:
- You expect third parties to add new subtypes (open hierarchy).
- The operation is fundamental to the type and belongs in the class.
- You have many operations that all need the same subtype-specific behaviour.
Switch pattern matching is better when:
- You own all the subtypes (closed/sealed hierarchy).
- The operation is external to the type (e.g., serialisation, rendering).
- You want compile-time exhaustiveness checking.
// Polymorphism — Shape knows how to compute its own area
interface Shape { double area(); }
// Switch — external function; exhaustiveness verified by compiler
double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.r() * c.r();
case Rectangle r -> r.w() * r.h();
};
}
Rule of thumb: sealed types + switch for operations owned by the caller; polymorphism for behaviour the type itself should own.
More Modern Java interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.