Double dispatch Created 2026-10-31 Updated 2026-11-05
Double dispatch selects behavior using the runtime types of two participating software objects, often by arranging two ordinary dynamic dispatch steps. The visitor pattern supplies a useful example.
Suppose
Shape has an overridable accept(Renderer) operation, and Renderer has overridable overloads visit(Circle) and visit(Rectangle). A Circle implementation of accept calls renderer.visit(this). Calling shape.accept(renderer) first selects Circle.accept using the shape's runtime type. Inside that method, this has the static type Circle because the method is defined in the Circle software class. Method overloading therefore chooses the visit(Circle) signature. The renderer's runtime type then selects the implementation of that signature.The result depends on both the concrete shape and concrete renderer. Ordinary method overloading alone would not inspect an argument's runtime type to choose the shape overload. The static type established in the first dispatched method is the bridge to the second dispatch.[ref]
Scala 2.11 expressions specification. Overloading Resolution, static argument typing; example uses ordinary overridable instance methods
Method overloading 2026-10-24
Several callable methods share one name but differ in parameter signatures. In ordinary Scala calls, the compiler chooses among applicable alternatives using the receiver and arguments' static types, preferring a uniquely most-specific applicable alternative. Overloading is therefore a different step from method overriding and receiver-based dynamic dispatch.
Numeric widening 2026-10-31
A language-defined conversion from a numeric type to another admitted numeric type, such as Scala's conversion of an
Int argument to Double. It can make an overloaded method applicable even when the argument's static type differs from the parameter type. It does not imply a subtyping relationship between every pair of numeric types or exact representation of every source value; sufficiently large integers can lose precision in a floating-point target. [ref]Scala 2.11 expressions specification. Numeric Widening
To analyze an ordinary overloaded instance call, first enumerate members visible through the receiver's static type and choose the applicable signature using argument static types. Only then use the receiver's runtime type to select the effective override of that signature. This two-stage rule explains why changing a reference's declared type can change the selected overload even when every reference points to the same object. A
super call selects the inherited implementation according to the language's superclass or linearization rules instead of recursively redispatching to the overriding body.Method overloading chooses a signature using the static types available at the call site. The receiver
m has static type M, whose visible overloads accept X and Y; the Z overload introduced only in N is not a candidate.For Equivalently,The runtime type of every argument is
m.f(x), the argument's static type is X, so the chosen signature is f(X). N does not override it, and the inherited M body runs. For m.f(y), the chosen signature is f(Y), and dynamic dispatch on the receiver chooses N's override. For m.f(z), both visible signatures apply, but f(Y) is more specific than f(X). Dynamic dispatch again chooses N.f(Y). Thusf with X at M
f with Y at N
f with Y at NZ, but ordinary single-receiver dynamic dispatch does not redo method overloading using that runtime type. This is overload selection followed by dynamic dispatch.Use double dispatch. Each original overload first sends a dynamically dispatched message to its argument. The argument's selected body then calls an overloaded receiver method with All three
this, whose static type inside that body is the desired argument software class.class X {
def sendTo(m: M): String = m.receive(this)
}
class Y extends X {
override def sendTo(m: M): String = m.receive(this)
}
class Z extends Y {
override def sendTo(m: M): String = m.receive(this)
}
class M {
def f(x: X): String = x.sendTo(this)
def f(x: Y): String = x.sendTo(this)
def receive(x: X): String = "f with X at M"
def receive(x: Y): String = "f with Y at M"
def receive(x: Z): String = "f with Z at M"
}
class N extends M {
override def f(x: Y): String = x.sendTo(this)
def f(x: Z): String = x.sendTo(this)
override def receive(x: X): String = "f with X at N"
override def receive(x: Y): String = "f with Y at N"
override def receive(x: Z): String = "f with Z at N"
}receive overloads must be declared in M, because sendTo receives a parameter with static type M. Declaring the Z version only in N would repeat the original problem.Whatever original The result depends on both runtime types:For the stated
f signature is selected, it invokes sendTo on the actual argument. For a Z object, dynamic dispatch enters Z.sendTo; inside that body, this has static type Z, so method overloading selects receive(Z). The final call dynamically dispatches on the actual m, giving N.receive(Z) when m refers to N. Hencef with Z at N
f with Z at N
f with Z at NX, Y, Z hierarchy, the same construction reports each argument software class correctly for either receiver software class. Further subclasses would need their own dispatch protocol to distinguish additional runtime types.Apply overload selection followed by dynamic dispatch: the receiver's static type determines visible signatures, and argument static types choose the overload. The receiver's runtime type then selects its override. A
super call starts at the inherited implementation instead of redispatching to the override just entered.For
c.f1(y), the Y overload is visible on C, so its body in C runs. For a.f2(x), the X signature is selected and its override in C calls C.f1(X). For a.f2(y), the Y signature is selected; its body in C calls super.f1(y). The superclass B has only f1(X), so that call uses B.f1(X).For
b.f3(y), the static type B exposes only f3(X). Its override in C calls super.f2(x), entering B.f2(X). The unqualified f1(x) inside that body still uses dynamic dispatch on the original C instance, so it calls C.f1(X).Finally
c.f4(x) invokes the companion object C, which is a separate receiver extending B. Its f3(X) calls inherited B.f2(X), whose f1 call dispatches to ObjC.f1(X).[ref]Imperative Programming examination paper by the University of Oxford (2014). p. 10, Question 6(a), method bodies and calls
Apply overload selection followed by dynamic dispatch: method overloading chooses a signature from the receiver's static type and argument static types, then dynamic dispatch chooses an override of that signature using the receiver's runtime type. A
super call selects the inherited implementation; a call on the companion object B uses that object's own methods.[ref]Scala 2.11 expressions specification. Static overload resolution; this and super
The output, in order, isFor
f(x:X) in B
f(y:Y) in A
f(x:X) in A, g(x:X) in B
f(x:X) in ObjB, g(y:Y) in ObjB, h(y:Y) in B, h(z:Z) in C
f(y:Y) in C, m(a:A) in Y, k(x:X) in Ca.f(x), the argument's static type is X, selecting f(X); the receiver's runtime type B selects B.f(X). For a.f(z), the receiver's static type A exposes only f(X) and f(Y). The Z argument fits both, and f(Y) is more specific. B does not override that signature, so A.f(Y) runs. The additional overload B.f(Z) is invisible through a.For
c.g(z), the inherited instance B.g(X) calls super.f(x), which is A.f(X). For c.h(z), method overloading selects C.h(Z). Its super.h(z) calls B.h(Y), where the local parameter has static type Y. Consequently B.g(y) selects g(Y) on the companion object, whose only f signature is f(X).Finally,
c.k(x) calls x.m(this). The receiver x has runtime type Y, so dynamic dispatch enters Y.m(A). Inside that method, this has static type Y, selecting a.f(Y); the actual argument object is c, so dynamic dispatch reaches C.f(Y). ThereforeThe five printed lines, including the comma and space after every fragment, are:Scala dynamic dispatch uses the receiver's runtime software class, even though
f1 in C,
f1 in C, f2 in B,
f1 in C, f2 in A, f3 in B,
f1 in A, f2 in A, f3 in ObjB, f4 in B,
f1 in ObjC, f2 in A, f3 in B, f4 in ObjC, f5 in C, a has the static type A. Thus a.f1 uses C.f1, and a.f2 uses B.f2 whose unqualified f1 still dispatches on the new C receiver.For
a.f3, B.f3 explicitly selects super.f2, which is A.f2. Inside A.f2, the unqualified call to f1 still dispatches to C.f1; super does not replace the receiver. For a.f4, B.f3 in the body of class B means a call on the singleton object B, not on this. That object's f3 calls A.f2, which calls A.f1.Finally
a.f5 calls C.f4 on the singleton object C. That object extends class B, overrides f1 and f4, and inherits class B's f3. Its f4 calls this inherited f3; that selects A.f2, whose f1 dispatches to the object's own override. These distinctions follow member selection and super semantics.[ref]Scala 2.11 expressions specification. This and Super; Designators, member selection
The output is
YY, with no newline. The variable a has static type A, although its runtime type is B. Both c.m(a) and d.m(a) therefore select the signature m(A) at compile time. The m(B) alternative is not applicable to an argument whose static type is merely A.Both receivers refer to the same
D object. Dynamic dispatch then invokes D's override of the already selected m(A) signature, printing Y twice. The static type C of c changes the visible member set, but both signatures are visible through either receiver in this example. Overload selection followed by dynamic dispatch does not select an overload from the argument's runtime type.The output is
BADDCD, without a newline. Method overloading chooses a signature using static types, including the instantiated class type parameter; numeric widening can make a Double parameter applicable to an Int argument.a.m(o1)choosesm(Int), which is more specific than the polymorphic method:B.a.m(o2)usesm[Double](Double):A.b.m(o1)comparesm(Any)andm(Double).Intcan widen toDouble, andDoubleis more specific thanAny:D.b.m(o2)choosesm(Double)overm(Any):D.c.m(o1)compares the instantiatedm(Int)withm(Double)and choosesm(Int):C.c.m(o2)can use onlym(Double):D.
The third call is the subtle one: the existence of
m(Any) does not prevent the more specific numeric overload from winning. These are Scala 2 method overloading and numeric widening rules. [ref]The output is
YXWX, without a newline. Apply overload selection followed by dynamic dispatch separately to each call:Although
a actually refers to a B, that runtime type does not select another overload. Dynamic dispatch chooses the implementation of the already selected signature on the receiver.Programming language polymorphism means that one interface or program definition can be used with values of multiple types. In object-oriented programming, subtype polymorphism allows references with a common static type to denote instances of different concrete subclasses or implementers of a trait. An object method call uses dynamic dispatch to select the overriding implementation belonging to the receiver's runtime type. For example, different implementations of a playlist interface can respond to the same
insert call without the client testing their concrete classes.Parametric polymorphism uses type parameters, as in
Sequence[A], so the same definition works uniformly for songs, integers, or other elements. Ad hoc polymorphism includes method overloading, where different parameter signatures share a name and the static types determine overload selection. These mechanisms are distinct: selecting an overloaded signature is not the same step as dispatching an override of that selected signature. Class inheritance can supply implementations, but the reusable interface and legal subtyping relationship determine what a polymorphic client may do.Method overloading gives one name several parameter signatures, such as
show(A) and show(B). The compiler chooses an applicable signature using the receiver's static type and the argument expressions' static types, preferring a uniquely more specific applicable alternative. An argument whose type is a subtype of the parameter type is acceptable; ambiguous or inapplicable alternatives cause a compile-time error. The runtime type of an argument does not start a second overload search.Method overriding replaces an inherited implementation of the same method signature. After overload selection, ordinary instance calls use dynamic dispatch on the receiver's runtime type to select the most-derived implementation of that signature. A concrete inherited method requires the
override keyword in Scala; a super call invokes the corresponding superclass implementation instead of redispatching to the overriding body. In a trait stack, super follows the class's linearization.[ref]Scala 2.13 expressions specification. Overloading resolution: applicable alternatives and most-specific selection
[ref]Scala 2.13 classes and objects specification. Member lookup, overriding and override modifier
Permitted; returns
"AB". A receiver of static type B exposes show(A) and show(B), but the argument's static type A is not a subtype of B. Only show(A) is applicable. Its effective implementation on the C receiver is B.show(A), which concatenates the superclass result "A" and "B".Permitted; returns
"AB". Through a receiver of static type A, only show(A) is visible. The argument's static type C is a subtype of A, so it can be passed to that method. Dynamic dispatch selects B.show(A) on the receiver object, exactly as in part (i). The argument's more specific type cannot expose overloads absent from the receiver's static type.Permitted; returns
"AB". The receiver's static type is A, which offers show(A). The argument also has static type A, so that signature is selected. All three variables refer to the same object of runtime type C; the most-derived implementation of show(A) is inherited from B. Its super.show(o) calls A.show(A), producing "A", and it appends "B". C overrides show(B), not this selected signature.Permitted; returns
"BC". Through static type B, both show(A) and show(B) accept an argument of type C; method overloading selects the more specific show(B). The receiver's runtime type is C, so dynamic dispatch invokes C.show(B). Its super.show(o) calls B.show(B), returning "B", and the override appends "C". The show(C) overload is not visible through receiver type B.Permitted; returns
"C". Both receiver and argument have static type C, so the most-specific applicable overload is show(C). That method is defined in C and returns its own one-letter result without calling super. The six results, in source order, are therefore AB, AB, AB, BC, BC, C.Permitted; returns
"BC". The receiver's static type is C, but the argument's static type is only B. Thus show(C) is inapplicable; among show(A) and show(B), the latter is more specific. Its implementation is C.show(B), whose superclass call returns "B" before "C" is appended. The argument happens to reference a C object, but method overloading uses its static type.