Skip to content

Switch Pattern Matching Interview Questions & Answers

14 questions Updated 2026-06-20 Share:

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.

Read the in-depth guideJava Switch Pattern Matching — Expressions, Type Patterns, and Record Deconstruction(opens in new tab)
14 of 14

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 ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel