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.
1class Animal {}2class Dog extends Animal {}34class Vet {5 String treat(Animal a) { return "Vet.treat(Animal)"; }6 String treat(Dog d) { return "Vet.treat(Dog)"; }7}89class ER extends Vet {10 @Override11 String treat(Animal a) { return "ER.treat(Animal)"; }12}v.treat(x);
- 1 · javac, compile timereads declared typesv 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 · JVM, run timereads the objectv points at a ER. does ER override treat(Animal)? yes.
- prints ER.treat(Animal)
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:
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));
}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
| overloading | overriding | |
|---|---|---|
| what | same name, different parameter lists | same signature, in a subclass |
| who decides | javac | the JVM |
| when | compile time | every call, at runtime |
| based on | declared types of the arguments | actual class of the receiver |
| in the bytecode | the exact signature is baked in | only the signature. the class is looked up live |
| works for statics? | yes | no, statics hide |
| works for fields? | n/a | no, 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.
// Dog.speak @Override String speak() { return "woof"; }
5/5jump to Dog.speak(). that's dynamic dispatch: the object picked the code.
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.
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
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.
show(Object) and show(String) both declared, what do show(null) and Object o = "hi"; show(o) print?- 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.