skip to content

Walk through refactoring an `if-else` chain of `instanceof` + cast into a pattern switch. What are the pitfalls (dominance, fall-through, null) to watch for?

level: seniorimportance: should knowfreq 45%

answer

  1. instanceof T t → case T t
  2. && condition → when guard
  3. Specific/guarded/subtype cases FIRST (dominance)
  4. Arrow form: no fall-through
  5. Add case null (switch NPEs by default); finish exhaustive

basics

~20 s

Replace each if (x instanceof T t) branch with a case T t -> label in a switch over x. Use guards (when) for the extra conditions, keep the most specific cases first, add case null if null is possible, and make it exhaustive with a default or full subtype coverage.

solid answer

~50 s

Start from the `instanceof` ladder: each `if (x instanceof Type t) { ... }` becomes `case Type t -> ...`, dropping the manual cast. Any extra `&&` condition in the branch becomes a `when` guard. Three pitfalls: (1) **dominance/order** — a broader type or unguarded case must come after narrower/guarded ones, since the first matching case wins and the compiler rejects unreachable cases; subtype cases must precede supertype cases. (2) **fall-through** — use the arrow form so branches don't fall through and bindings stay scoped; the old colon form with patterns is error-prone. (3) **null** — an `if`-ladder typically reaches a final `else` for null, but a pattern switch throws NPE on null unless you add `case null`, so port that null handling explicitly. Finally make the switch exhaustive: a `default` for open types, or all permitted subtypes for a sealed type (preferably no default so future variants force a recompile). The result is shorter, cast-free, and compiler-checked for completeness.

code

java · 19 lines
java
// Before: instanceof + cast ladder
String format(Object o) {
    if (o instanceof Integer i && i > 0) return "positive int " + i;
    else if (o instanceof Integer i)    return "non-positive int " + i;
    else if (o instanceof String s)     return "string " + s;
    else if (o == null)                 return "null";
    else                                return "other";
}

// After: pattern switch
String formatSwitch(Object o) {
    return switch (o) {
        case null                 -> "null";
        case Integer i when i > 0 -> "positive int " + i;
        case Integer i            -> "non-positive int " + i;
        case String s             -> "string " + s;
        default                   -> "other";
    };
}

go deeper

for a junior

Can mechanically turn instanceof + cast branches into case Type t labels.

for a middle

Handles guards for extra conditions, basic ordering, and remembers to add case null and a default.

for a senior

Anticipates dominance errors, arrow-vs-colon fall-through, null behavior change, and exhaustiveness; produces clean, cast-free switches.

for a principal

Drives migrations across a codebase, chooses sealed hierarchies to remove defaults, and sets conventions (arrow form, side-effect-free guards, null-at-boundary policy) for maintainability.

## The starting point A common pre-pattern idiom branches on runtime type with `instanceof` and a cast: ```java String format(Object o) { if (o instanceof Integer i && i > 0) { return "positive int " + i; } else if (o instanceof Integer i) { return "non-positive int " + i; } else if (o instanceof String s) { return "string " + s; } else if (o == null) { return "null"; } else { return "other"; } } ``` (`instanceof Integer i` is *pattern* `instanceof`: it tests the type and binds `i` if it matches.) ## Step-by-step refactor **1. Switch over the value.** `switch (o) { ... }`. **2. Each `instanceof` branch → a `case` with a type pattern.** Drop the explicit cast; the binding gives you a typed variable: ```java case String s -> "string " + s; ``` **3. Extra conditions → guards.** `if (o instanceof Integer i && i > 0)` becomes: ```java case Integer i when i > 0 -> "positive int " + i; ``` **4. Port the null branch.** The `else if (o == null)` cannot be expressed by any type pattern (type patterns never match null), and a switch throws NPE on null by default — so add an explicit `case null`. **5. Port the final `else` → `default`.** Result: ```java String format(Object o) { return switch (o) { case null -> "null"; case Integer i when i > 0 -> "positive int " + i; case Integer i -> "non-positive int " + i; case String s -> "string " + s; default -> "other"; }; } ``` ## Pitfall 1: dominance and ordering Cases are matched **top to bottom; first match wins.** So: - The **guarded** `case Integer i when i > 0` must precede the **unguarded** `case Integer i`; otherwise the unguarded one *dominates* (catches all integers first) and the guarded case is unreachable — a **compile error**. - A **subtype** case must precede a **supertype** case (e.g. `case ArrayList<?> a` before `case List<?> l`), for the same reason. - The compiler enforces this by rejecting dominated (unreachable) cases. ## Pitfall 2: fall-through Use the **arrow form** (`case L -> ...`). It does not fall through and scopes bindings to the branch. The legacy **colon form** (`case L:`) falls through unless you `break`, and mixing it with pattern bindings creates subtle scope/flow bugs — avoid it for pattern switches. ## Pitfall 3: null semantics changed The `if`-ladder usually handles null in some branch (often the final `else`). A switch does **not**: null throws NPE unless you add `case null` (or `case null, default`). Forgetting this silently changes behavior from "handled" to "throws" — port the null branch deliberately. ## Pitfall 4: exhaustiveness As a switch *expression*, it must be exhaustive. Add a `default` for open types. If `o`'s type is **sealed**, prefer covering every permitted subtype and **omitting default**, so adding a future subtype forces a recompile rather than silently hitting default. ## Benefits realized - No manual casts (bindings are pre-typed). - Conditions are colocated with their type via guards. - Completeness is compiler-checked. - Shorter, flatter, and easier to read than the `if` ladder. ## Deriving an answer Map each `instanceof`-branch to a `case` type pattern (cast disappears), `&&` conditions to `when` guards, order specific/guarded before general (dominance), use arrow form (no fall-through), add `case null` because switch NPEs on null, and finish with a default or full sealed coverage for exhaustiveness.

  • After refactoring, the compiler complains the unguarded `case Integer i` is unreachable. Why?
    It is placed after a case that already matches all integers (e.g. an earlier unguarded `case Number n`, or the order of guarded/unguarded was swapped). The earlier case dominates it. Reorder so the more specific or guarded cases come first.
  • Behavior changed: the method now throws on null where it used to return "null". What was missed?
    The null branch wasn't ported. A switch throws NullPointerException on a null selector unless you add an explicit `case null` (or `case null, default`).

saying these in an interview costs you the question

  • Leaving the old null else-branch behind, so the switch now NPEs
  • Placing the unguarded/supertype case before the guarded/subtype one (dominance error)
  • Using the colon form and getting fall-through bugs
  • Forgetting exhaustiveness — a switch expression won't compile without full coverage
  • Keeping redundant casts inside the case body

context