Method dispatch: walking up from the receiver
Follow one call up the inheritance chain from the receiver's own class to the first body that implements the selected signature. Inheritance and polymorphism · Object-oriented programming.
A support app sends one notice through a variable typed Notice, but the object in memory is an UrgentEmail. The object you call a method on is the receiver, here the UrgentEmail. The call line is the same whatever the variable's type says. The question is where the program looks for the send body that runs.
By now the call has its signature, send(String); the overloading lesson showed how. This lesson asks only where the body is looked up.
Walk up from the receiver
class Notice {
void send(String s) { System.out.println("base:" + s); }
}
class EmailNotice extends Notice {
@Override void send(String s) { System.out.println("email:" + s); }
}
class UrgentEmail extends EmailNotice {}
Notice n = new UrgentEmail();
n.send("status"); // email:status
Notice m = new Notice();
m.send("status"); // base:statusLookup starts at the object's own class, UrgentEmail, not at the declared type Notice. UrgentEmail has no send(String), so the search climbs to EmailNotice, which does, and that body runs. The same line on a plain Notice object stops at Notice. The variable's type never starts the search. For an instance method it only decides which signatures the compiler lets you call on that variable; the receiver's runtime class then decides which body is found for the signature the compiler selected. Two calls made through the same declared type can therefore reach different bodies.
Every body reached this way runs on the same object. Inside EmailNotice.send, this is still the UrgentEmail you called it on; climbing the chain changes which code runs, never who the receiver is.
Java: the search climbs from the receiver's class
Read the steps as text
- UrgentEmail extends EmailNotice, which extends Notice. Notice declares send(String). EmailNotice declares send(String). UrgentEmail declares no methods of its own. No call has run yet.
- The call n.send("status") starts. The variable n is typed Notice and holds object E7, an UrgentEmail. The signature send(String) is already selected.
- The search for send(String) starts at the receiver's own class, UrgentEmail, and not at the declared type Notice.
- UrgentEmail has no send(String), so the search goes up to its parent.
- EmailNotice implements send(String), so the search stops there.
- The body of EmailNotice.send(String) runs with this set to E7, the same object the call started on.
- The body of EmailNotice prints email:status, and the call is finished.
- The call m.send("status") starts. The variable m is typed Notice and holds object N2, a Notice. The signature to find is send(String).
- The search for send(String) starts at the receiver's own class, Notice, which is also the declared type this time. Notice implements send(String), so the search stops there.
- The body of Notice.send(String) runs with this set to N2, the same object the call started on.
- The body of Notice prints base:status, and the call is finished.
super restarts one level up
class EmailNotice extends Notice {
@Override void send(String s) {
super.send(s);
System.out.println("email:" + s);
}
}
// UrgentEmail receiver: prints base:status, then email:statussuper.send(s) is a fresh lookup that starts at the parent of the class whose code contains the call, here Notice. It does not start at the receiver's class and not at the receiver's parent. It still runs on the same receiver.
Java: super restarts the search one class up
Read the steps as text
- UrgentEmail extends EmailNotice, which extends Notice. Notice declares send(String), with no super call. EmailNotice declares send(String), and calls super.send(s). UrgentEmail adds no send body.
- The call n.send("status") on object E7 has already climbed from UrgentEmail to EmailNotice, the first class implementing send(String). The body of EmailNotice is running with this set to E7.
- The body of EmailNotice calls super.send(s). A super call starts at the parent of the class holding the call, Notice, and the receiver stays E7.
- Notice implements send(String), so the restarted search stops there.
- The body of Notice.send(String) runs, and this is still E7, although the code now lives in Notice.
- The body of Notice prints base:status, and control returns to the body of EmailNotice.
- The body of EmailNotice prints email:status once the super call has come back, and the call is finished.
Python reads the order off the object
class Notice:
def send(self, label): return "base:" + label
class EmailNotice(Notice):
def send(self, label): return "email:" + label
class UrgentEmail(EmailNotice): pass
n = UrgentEmail()
print([c.__name__ for c in type(n).__mro__])
# ['UrgentEmail', 'EmailNotice', 'Notice', 'object']
print(n.send.__self__ is n) # True
print(n.send("status")) # email:statusPython reads the lookup order, called the method resolution order (MRO), off the object: type(n).__mro__ lists UrgentEmail, EmailNotice, Notice, object, and the first class defining send supplies the function. Reading n.send then binds that function to n, so self inside it is the same object as n, whichever class the function was found in. The search is first match wins: classes later in the list are never consulted once an earlier one defines the name. super().send(label) inside EmailNotice continues after EmailNotice in this list:
class EmailNotice(Notice):
def send(self, label): return super().send(label) + " then email:" + label
# UrgentEmail().send("status") returns base:status then email:statusPython: the search follows the object's MRO
Read the steps as text
- UrgentEmail inherits from EmailNotice, which inherits from Notice, which inherits from object. Notice declares send. EmailNotice declares send. UrgentEmail and object define no send of their own. No call has run yet.
- The call print(n.send("status")) starts. The variable n holds object E7, an instance of UrgentEmail. Python looks the name send up on that object.
- Python starts at type(n), the class UrgentEmail, and reads the order from its __mro__: UrgentEmail, EmailNotice, Notice and object.
- UrgentEmail has no send, so the search moves on to the next class in the MRO.
- EmailNotice implements send, so the search stops there.
- Reading n.send binds the function found in EmailNotice to object E7, so self will be E7, not EmailNotice.
- The body of EmailNotice.send runs with self set to E7, the same object the call started on.
- The body of EmailNotice returns email:status, and print shows it.
A short C++ boundary
In C++ a virtual call follows the object it points to, while a non-virtual call follows the pointer's own type. A call through a Base pointer to a virtual member reaches the final overrider for the object's dynamic type, which is its actual class; override asks the compiler to reject a signature mismatch. A call to a non-virtual member uses the member the pointer type names, even if the object is a Derived. A qualified call such as p->Base::speak() requests the base body directly.
C++: virtual, non-virtual and qualified calls
Read the steps as text
- EmailNotice derives from Notice. Notice declares send() and tag(). EmailNotice declares send() and tag(). No call has run yet.
- The call p->send() starts. The pointer p is a Notice pointer (Notice*) that points to object E7, an EmailNotice. The signature send() is already selected.
- The member send is virtual, so the search starts at the object's own class, EmailNotice, its dynamic type. EmailNotice implements send(), so the search stops there.
- The body of EmailNotice::send() runs with this set to E7, the same object the call started on. The body of EmailNotice prints email-send, and the call is finished.
- The call p->tag() starts. The pointer p still points to object E7, and this call uses the member tag. The signature to find is tag().
- The member tag is not virtual, so the search starts at the pointer's own type, Notice, whatever object it points to. Notice implements tag(), so the search stops there.
- The body of Notice::tag() runs with this set to E7, the same object the call started on. The body of Notice prints base-tag, and the call is finished.
- The call p->Notice::send() starts. The pointer p still points to object E7, and this call uses the member send. The signature to find is send().
- The call names Notice in front of the member, so the search starts there and skips the receiver's own class. Notice implements send(), so the search stops there.
- The body of Notice::send() runs with this set to E7, the same object the call started on.
- The body of Notice prints base-send, and the call is finished.
Many compilers carry out the virtual choice with a hidden pointer (vptr) into a table of functions (vtable), but that is a common implementation model, not something the language requires.
In practice this is how you reason when you add a class in the middle of a hierarchy: callers below it now stop at the new class's send instead of reaching the one above, and calling super.send(...) is how the new body keeps the shared behaviour instead of copying it.
Recap
- Lookup starts at the receiver's own class and climbs to the first class that implements the selected signature.
- Every body reached runs on the same receiver;
super.m()restarts the climb at the parent of the class containing the call. - Python walks
type(obj).__mro__and binds the found function to the object; C++ gives the final overrider only for virtual calls, and vtables are a common model, not a guarantee.
Go deeper
Python's super() continues after the current class in the MRO; it is not a promise to call a textual parent by itself, which matters once a class has more than one parent path. In C++, a qualified call such as p->Base::speak() is also how a derived override reuses the base body.
Three kinds of method skip the receiver's-class walk or make it trivial: a static method is chosen by the declared type, a private method by the class containing the call, and a final method has no override to find, so the walk can only end at one body.
Continue to the inheritance diamond, the last lesson of this module. It asks where the search goes when a class has more than one parent path, which a single chain like this one cannot show.
To practise telling a reference from the object it reaches, which is what the receiver is, trace a pointer chain in Linked List Fundamentals.