skip to content
everything.oop.polymorphism

polymorphism and dynamic dispatch

overloading picks at compile time, overriding picks at runtime. the vtable.

chapter
chapter 03.07
reading time
7 min read

two decisions, made at two different times. overloading (same name, different parameters) is resolved by javac, at compile time, from the declared types. overriding (a subclass replacing a method) is resolved by the JVM, at runtime, from the actual class of the object you call it on.

that second one is polymorphism. one call site, different code depending on the object.

the picture

you know how Animal a = new Dog() has two types at once? kinda confusing until you name them. Animal is the static type (what the variable is declared as, what javac sees). Dog is the runtime type (what the object actually is, what the JVM sees).

think of a hospital. reception (javac) reads your form and sends you to a department based on what's written on it ("animal, needs treatment"). once you're there, whoever is on shift (the object) does the actual work their way. reception never meets the doctor. the doctor never re-reads your form.

v =
x =
1class Animal {}
2class Dog extends Animal {}
3
4class Vet {
5 String treat(Animal a) { return "Vet.treat(Animal)"; }
6 String treat(Dog d) { return "Vet.treat(Dog)"; }
7}
8
9class ER extends Vet {
10 @Override
11 String treat(Animal a) { return "ER.treat(Animal)"; }
12}
v.treat(x);
  1. 1 · javac, compile timereads declared types
    v is a Vet, x is an Animal. the best match in Vet is treat(Animal). it can't know x will hold a Dog.
    bytecode: invokevirtual Vet.treat(Animal)
  2. 2 · JVM, run timereads the object
    v points at a ER. does ER override treat(Animal)? yes.
  3. prints ER.treat(Animal)
pick the vet and the argument. amber is the overload javac chose from declared types. green is the code that actually ran. try new ER() with Dog x: the emergency vet falls back to the plain vet's method, because it never overrode that overload.

here's every combination, run for real:

Main.java
class Animal {}
class Dog extends Animal {}
 
class Vet {
    String treat(Animal a) { return "Vet.treat(Animal)"; }
    String treat(Dog d)    { return "Vet.treat(Dog)"; }
}
 
class ER extends Vet {
    @Override
    String treat(Animal a) { return "ER.treat(Animal)"; }
}
 
void main() {
    Vet v1 = new Vet();   Vet v2 = new ER();
    Animal a1 = new Animal(); Animal a2 = new Dog(); Dog d = new Dog();
    IO.println("Vet/Animal(Animal) " + v1.treat(a1));
    IO.println("Vet/Animal(Dog) " + v1.treat(a2));
    IO.println("Vet/Dog(Dog) " + v1.treat(d));
    IO.println("ER/Animal(Animal) " + v2.treat(a1));
    IO.println("ER/Animal(Dog) " + v2.treat(a2));
    IO.println("ER/Dog(Dog) " + v2.treat(d));
}
output
Vet/Animal(Animal) Vet.treat(Animal)
Vet/Animal(Dog) Vet.treat(Animal)
Vet/Dog(Dog) Vet.treat(Dog)
ER/Animal(Animal) ER.treat(Animal)
ER/Animal(Dog) ER.treat(Animal)
ER/Dog(Dog) Vet.treat(Dog)

look at lines 2 and 5 of the output: the argument was a Dog at runtime, and nobody cared. java only dispatches on the receiver (the thing before the dot). that's called single dispatch. the arguments' runtime classes never pick the method.

side by side

overloadingoverriding
whatsame name, different parameter listssame signature, in a subclass
who decidesjavacthe JVM
whencompile timeevery call, at runtime
based ondeclared types of the argumentsactual class of the receiver
in the bytecodethe exact signature is baked inonly the signature. the class is looked up live
works for statics?yesno, statics hide
works for fields?n/ano, fields hide

how the JVM finds the method

javac writes invokevirtual Animal.speak() into the class file. that names a method, not an implementation. so at runtime the JVM needs to get from "this object" to "the right code", and it needs to do it fast, because this happens billions of times.

the answer is the vtable: every class has a table of its overridable methods. a subclass's table starts as a copy of its parent's, overrides replace their entry in place, and new methods go on the end. so speak sits in the same slot in Animal's table and Dog's table. the call doesn't search anything, it reads one slot.

a =
call =
stack
main
Animal a
heap
Dog
mark…
class
fields…
class Dog · vtable
0…Object.toStringinherited
…equals, hashCode, clone… → Object
kDog.speakoverrides
k+1Animal.eatinherited
k+2Dog.fetchnew
code that runs
// Dog.speak
@Override
String speak() {
    return "woof";
}

5/5jump to Dog.speak(). that's dynamic dispatch: the object picked the code.

pick an object and a call, and watch the lookup: reference to the object, the header's class pointer to its class, one vtable slot, then the code. switch between new Animal() and new Dog(): same slot, different table, different code.

not every call goes through this. static methods (invokestatic), private methods, constructors and super.x() calls (invokespecial) are bound to exactly one implementation, so there's nothing to look up. interface calls use invokeinterface and a separate table, the itable. the full map is in the five invoke instructions.

one level deeper:the JIT usually skips the vtable entirely+

a vtable call is cheap, but it still blocks inlining, because the JIT can't paste in a body it doesn't know. so HotSpot profiles every call site. if a call site has only ever seen Dog (monomorphic), C2 compiles a guard ("is this a Dog?") followed by Dog.speak() inlined right there. two classes (bimorphic) gets two guards. past that (megamorphic) it falls back to the real vtable call.

class hierarchy analysis goes further: if only one loaded class implements a method at all, the JIT treats the call as direct, and deoptimizes if a new subclass is ever loaded. so a final keyword is not a performance trick in modern java, the JIT figures it out anyway. more in JIT optimizations.

the classic trick question

predict the output
java
class A {
    String who(A a) { return "A.who(A)"; }
}
 
class B extends A {
    @Override
    String who(A a) { return "B.who(A)"; }
    String who(B b) { return "B.who(B)"; }
}
 
void main() {
    A ab = new B();
    B b = new B();
    IO.println(ab.who(b));
}

if you got it wrong, run it through the two-step rule again: first find the signature from the declared types, then find the override from the object. interviewers love this one because people try to do both steps at once.

quick check
with show(Object) and show(String) both declared, what do show(null) and Object o = "hi"; show(o) print?
keylines
  • static type: what the variable says. runtime type: what the object is.
  • overloads are picked by javac from static types. overrides are picked by the JVM from the object.
  • java dispatches on the receiver only. argument objects never choose the method.
  • a vtable makes a virtual call one slot read. the JIT usually inlines it away.
  • static, private, constructor and super calls aren't dynamically dispatched.