Skip to content

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

java
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.

code
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:

KindBound
static methodscompile time
private methodscompile time
final methodscompile time
Fields, alwayscompile time
Overridable methodsrun time

Fields are the surprising row:

java
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.