Why does the Visitor pattern rely on double dispatch, and how is it implemented in Java?
answer
- Single dispatch = receiver type only; args resolved statically
- Need two runtime types -> double dispatch
- accept makes the element the receiver
- Two chained virtual calls
- v.visit(this) — this is statically the concrete type
basics
~20 sJava picks which method runs based on only one object's runtime type (the receiver). Visitor needs to choose based on two: the element type and the visitor type. The accept method does this in two steps — first dispatch on the element, then on the visitor — giving 'double dispatch'.
solid answer
~40 sJava is single-dispatch: at runtime, the actual method invoked is chosen by the runtime type of the *receiver* only; method *arguments* are resolved by their *compile-time* (static) types via overload resolution. Visitor needs the behaviour to depend on two runtime types — the concrete element AND the concrete visitor. The accept method bridges this. Calling element.accept(visitor) is the first virtual dispatch: it resolves to the concrete element's accept, where `this` is now statically known to be e.g. AddNode. Inside, it calls visitor.visit(this), a second virtual dispatch that picks visit(AddNode) on the concrete visitor. Two chained single dispatches = double dispatch. Without accept, a single visitor.visit(element) where element is typed as the base Element would, due to static overload resolution, always bind to visit(Element) (or fail to compile), never the specific overload.
code
java · 26 linesinterface Visitor {
int visit(NumberNode n);
int visit(AddNode n);
}
interface Node { int accept(Visitor v); }
final class NumberNode implements Node {
final int value;
NumberNode(int value) { this.value = value; }
public int accept(Visitor v) { return v.visit(this); } // this is statically NumberNode
}
final class AddNode implements Node {
final Node left, right;
AddNode(Node left, Node right) { this.left = left; this.right = right; }
public int accept(Visitor v) { return v.visit(this); } // this is statically AddNode
}
final class EvalVisitor implements Visitor {
public int visit(NumberNode n) { return n.value; }
public int visit(AddNode n) { return n.left.accept(this) + n.right.accept(this); }
}
// Node tree = new AddNode(new NumberNode(2), new NumberNode(3));
// int result = tree.accept(new EvalVisitor()); // 5go deeper
Knows the term 'double dispatch' means the called method depends on two objects' types, and that accept() plus visit() implement it.
Can walk through the two-step accept->visit call and explain that Java resolves overloads at compile time.
Precisely distinguishes single (runtime receiver) from overload resolution (compile-time args), and explains why the naive direct visit() call misbinds.
Connects double dispatch to language design (multiple dispatch in other languages), and evaluates pattern-matching switch as the runtime-type-branching replacement.
## Background: what 'dispatch' means **Dispatch** is the act of deciding, at a call site, *which actual method body runs*. **Single dispatch** (what Java does for virtual methods): the chosen method depends on the runtime type of exactly one object — the **receiver**, the thing left of the dot in `receiver.method(args)`. Example: `animal.speak()` runs `Dog.speak` if `animal` is really a `Dog`, regardless of the declared type. This is **dynamic dispatch** / virtual method invocation. **Crucial Java rule:** method *arguments* do **not** participate in runtime dispatch. When a method is *overloaded* (same name, different parameter types), Java chooses the overload at **compile time** using the **static (declared) types** of the arguments — this is **overload resolution**. The runtime type of an argument is ignored. ## Why Visitor needs *two* types The behaviour we want depends on a *pair*: which concrete element (`NumberNode` vs `AddNode`) and which concrete operation (`PrintVisitor` vs `EvaluateVisitor`). Choosing behaviour from two runtime types is **double dispatch**. Java has no built-in double dispatch, so Visitor *simulates* it with two chained single dispatches. ## The naive (broken) attempt Suppose you write: ```java Element e = new AddNode(...); // static type Element, runtime type AddNode Visitor v = new PrintVisitor(); v.visit(e); // which visit runs? ``` You might hope `visit(AddNode)` runs. It does **not**. `e`'s *static* type is `Element`, and overload resolution happens at compile time on that static type — so this either binds to `visit(Element)` (if it exists) forever, or fails to compile (if only the specific overloads exist). The runtime `AddNode`-ness of `e` is invisible to overload resolution. ## The fix: accept() turns the argument into the receiver Give every element an `accept` method and have it call back: ```java interface Element { void accept(Visitor v); } class AddNode implements Element { public void accept(Visitor v) { v.visit(this); // 'this' is statically AddNode here! } } ``` Now: ```java e.accept(v); ``` - **First dispatch (single, virtual):** `e.accept(...)` — the receiver is `e`, runtime type `AddNode`, so `AddNode.accept` runs. The element's concrete type has now been *resolved into the receiver position*. - **Second dispatch (single, virtual):** inside `AddNode.accept`, `this` is statically typed `AddNode`, so `v.visit(this)` performs overload resolution to the `visit(AddNode)` *signature* at compile time, and then virtual dispatch on `v` selects the concrete visitor's body (`PrintVisitor.visit(AddNode)` vs `EvaluateVisitor.visit(AddNode)`). Two single dispatches, chained, select behaviour from *both* runtime types. That is double dispatch, hand-rolled. ## The mental model Think of `accept` as a tiny adapter that says: "I know my own concrete type at compile time *inside my own method*, so let me hand that knowledge to the visitor as a precisely-typed `this`." Each element pays a one-line `accept` tax so visitors can be precisely typed. ## Common pitfalls - Forgetting `accept` and calling `visit(element)` directly — silently binds to the wrong overload. - Thinking Java resolves overloads by runtime type — it never does. - Copy-pasting `accept` but calling the wrong overload inside (e.g. casting). Keep it literally `v.visit(this)`. Modern Java sidesteps all of this with sealed types + pattern-matching `switch`, which can branch on the *runtime* type directly (covered separately).
- What happens if you call visitor.visit(element) directly where element's static type is the base Element?Overload resolution at compile time binds to visit(Element) (or fails to compile if only specific overloads exist); the runtime subtype is ignored, so you get the wrong/general behaviour — which is exactly why accept() is required.
- Could you fake double dispatch without accept(), using instanceof chains?Yes, a chain of instanceof/casts in one visit(Element) method works but loses compile-time exhaustiveness and re-centralises the type switch — essentially abandoning the pattern's structure. Modern pattern-matching switch makes this clean and checked.
saying these in an interview costs you the question
- Claiming Java overload resolution uses the runtime type of arguments
- Saying Java natively supports double dispatch
- Implementing accept with a cast or instanceof instead of v.visit(this)
- Confusing dynamic dispatch (1 type) with double dispatch (2 types)