12 · 32 · 5 min
How the right method gets found
Polymorphism looks like magic from the outside. Underneath it is one extra pointer and one extra lookup.
What happens
Animal a = new Dog();
a.speak();The compiler knows a is an Animal and cannot know it holds a Dog. So it cannot write a direct call. Instead it emits a lookup.
Each class with virtual methods has a virtual table: an array of function pointers, one per overridable method. Every object carries a hidden pointer to its class's table.
Dog object Dog vtable
+-----------+ +------------------+
| vptr ----+-----> | speak -> Dog.speak
| name | | eat -> Animal.eat
+-----------+ +------------------+Calling speak means: follow the object's table pointer, read slot 0, call it. Two memory reads and an indirect jump.
What it costs
A few nanoseconds, which is usually irrelevant. The larger cost is indirect: the compiler cannot inline a call whose target it does not know, and inlining is what enables most other optimisations.
Modern runtimes claw this back. A JIT observes that a call site has only ever seen Dog, compiles a direct inlined call with a check, and falls back if something else appears. This is why a Java loop calling a virtual method in a hot path can run at the speed of a direct call.
Static binding
Not everything is dispatched dynamically:
| Kind | Bound |
|---|---|
static methods | compile time |
private methods | compile time |
final methods | compile time |
| Fields, always | compile time |
| Overridable methods | run time |
Fields are the surprising row:
class Base { String name = "base"; }
class Derived extends Base { String name = "derived"; }
Base b = new Derived();
System.out.println(b.name); // "base"Fields are hidden, not overridden. Which one you get depends on the declared type, which is another reason to keep fields private and expose behaviour instead.
Across languages
C++ dispatches dynamically only for methods marked virtual, so you pay only where you asked for it. A non virtual method called through a base pointer runs the base version.
Python looks the method up in a dictionary at every call, following the method resolution order. Slower and completely uniform, and it is why monkey patching works.
Rust has no inheritance and uses trait objects with a vtable when dynamic dispatch is asked for, and generic monomorphisation otherwise.