instanceof pattern matching makes type-checking chains easy to write. When is a chain of instanceof patterns a code smell, and when is it the right tool over polymorphism?
answer
- Own + intrinsic → polymorphism
- Don't own / external op → patterns
- Replace Conditional with Polymorphism (smell)
- sealed + switch → compiler-checked exhaustiveness
- Exhaustiveness flips the weakness into a guarantee
basics
~20 sIf you own the type hierarchy and the behavior belongs to the objects, prefer polymorphism (a method each subtype overrides). Long instanceof chains are a smell there. instanceof patterns are right when you can't change the types, when adding behavior would pollute them, or over sealed types with switch.
solid answer
~60 sinstanceof patterns reduce the friction of type-dispatch, which makes it tempting to scatter `if (x instanceof A a) ... else if (x instanceof B b) ...` chains. The classic OO guidance still holds: if you own the hierarchy and the behavior is intrinsic to the objects, put it in a virtual method and let polymorphism dispatch — that keeps each type's behavior cohesive and lets you add subtypes without editing call sites. A long instanceof chain duplicates that dispatch at every call site and is fragile to new subtypes. However, patterns are the right tool when you cannot or should not modify the types (third-party or generated classes), when the operation doesn't belong on the domain object (serialization, a visitor-like external operation), or when you have a sealed hierarchy and want an exhaustive switch that the compiler checks. With sealed interfaces + switch pattern matching, the compiler enforces exhaustiveness, turning the 'fragile to new subtypes' weakness into a compile-time guarantee — which is often superior to scattered polymorphism for external operations.
code
java · 14 linessealed interface Shape permits Circle, Square, Rect {}
record Circle(double radius) implements Shape {}
record Square(double side) implements Shape {}
record Rect(double w, double h) implements Shape {}
// External operation over a closed set: exhaustive, compiler-checked.
static double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s -> s.side() * s.side();
case Rect r -> r.w() * r.h();
// no default needed; adding a Shape subtype forces an update here
};
}go deeper
Knows that overriding a method (polymorphism) is usually preferred over a long chain of instanceof checks.
Can name 'Replace Conditional with Polymorphism' and identify when you can't modify the types so patterns are needed.
Weighs ownership, cohesion, and cross-cutting concerns, and knows sealed + switch gives exhaustiveness.
Drives the architectural decision: closes hierarchies with sealed types, chooses external pattern-switches for cross-cutting ops to gain compiler-checked exhaustiveness, and sets team conventions distinguishing intrinsic vs external behavior.
## The temptation Because `instanceof` patterns remove the boilerplate cast, writing a type-dispatch chain is now very cheap: ```java if (shape instanceof Circle c) return Math.PI * c.radius() * c.radius(); else if (shape instanceof Square s) return s.side() * s.side(); else if (shape instanceof Rect r) return r.w() * r.h(); ``` Low friction can encourage a structure that the OO tradition warns against. ## Polymorphism: the default for owned, intrinsic behavior **Polymorphism** is when a method call dispatches to different implementations based on the object's runtime type, via virtual method overriding: ```java sealed interface Shape permits Circle, Square, Rect { double area(); // each subtype overrides } ``` When the behavior is **intrinsic to the object** (area *is* a property of a shape) and you **own the hierarchy**, polymorphism wins: each type's behavior lives with the type (cohesion), and adding a new `Shape` forces you to implement `area()` — you can't forget a case. A long `instanceof` chain, by contrast, repeats the dispatch at every call site and silently does the wrong thing (or nothing) when a new subtype appears. This is the classic refactoring **"Replace Conditional with Polymorphism."** A chain of `instanceof` that just calls a behavior the object could provide is a **code smell**. ## When instanceof patterns are the right tool 1. **You don't own the types.** Third-party, generated, or JDK classes you cannot add methods to. You can't put behavior on `java.time.Temporal` subtypes you didn't write; external dispatch is the realistic option. 2. **The operation doesn't belong on the domain object.** Cross-cutting concerns like serialization, rendering, or pretty-printing would pollute the domain model if pushed onto every type. Keeping them external (a visitor-style operation) is cleaner — and pattern matching is a lightweight visitor. 3. **Heterogeneous data with no shared interface.** Parsing/handling `Object` values from JSON, config, or reflection where there's no common method to call. 4. **Sealed hierarchies + switch pattern matching.** This is the strongest case. A **sealed** interface lists all its permitted subtypes, so the compiler knows the complete set. A `switch` over patterns can then be **exhaustive** — the compiler errors if you miss a case and forces you to revisit every switch when a subtype is added: ```java double area = switch (shape) { case Circle c -> Math.PI * c.radius() * c.radius(); case Square s -> s.side() * s.side(); case Rect r -> r.w() * r.h(); }; // exhaustive: no default needed, compiler-checked ``` Here the usual weakness of type-dispatch (forgetting a new subtype) becomes a **compile-time guarantee**. For *external* operations over a closed set of types, this is often *better* than polymorphism, because it co-locates one operation's logic and is checked. ## The decision framework - Own the types **and** behavior is intrinsic → **polymorphism**. - Don't own the types, or behavior is external/cross-cutting → **patterns** (instanceof or switch). - Closed set you own but the operation is external → **sealed + exhaustive switch patterns** (best of both: cohesion of the operation + compiler exhaustiveness). - Open-ended `if/else if instanceof` over an owned, growing hierarchy with no exhaustiveness → **smell**; refactor. ## Terms recap - **Polymorphism / virtual dispatch**: same call, different implementation by runtime type. - **Cohesion**: keeping related behavior together. - **sealed type**: a class/interface that explicitly lists its permitted subtypes, closing the hierarchy. - **Exhaustiveness**: the compiler verifying every possible case is handled. - **Visitor**: a pattern for adding external operations over a type hierarchy.
- How does a sealed interface change the trade-off between instanceof chains and polymorphism?It closes the hierarchy, so a switch over its patterns can be exhaustive and compiler-checked. That removes the main downside of external dispatch (forgetting a subtype), making pattern switches a strong, safe alternative to polymorphism for external operations.
- Give a case where polymorphism is clearly the wrong tool.Adding JSON serialization logic to every domain class: it pollutes the model with a cross-cutting concern, couples the domain to a format, and is hard to vary per context. An external, pattern-based serializer keeps the concern separate.
saying these in an interview costs you the question
- Treating instanceof chains as always bad, even for third-party types
- Treating polymorphism as always superior, ignoring external/cross-cutting operations
- Not mentioning sealed types + exhaustive switch as the modern resolution
- Believing the open if/else if chain gives any exhaustiveness checking