How does dynamic method dispatch work in Java, and what exactly does the runtime look at?
Dynamic dispatch is the mechanism behind overriding. When you call an instance method, the compiler checks that the declared type has a matching method and then generates a call that looks the method up in the object's actual class at run time. The declared type decides what is legal; the runtime type decides what runs.
public class Main {
public static void main(String[] args) {
Animal a = new Dog();
System.out.println(a.speak());
Animal b = new Cat();
System.out.println(b.speak());
speakTwice(a);
}
static void speakTwice(Animal animal) {
System.out.println(animal.speak());
System.out.println(animal.speak());
}
}
abstract class Animal {
abstract String speak();
}
class Dog extends Animal {
String speak() { return "woof"; }
}
class Cat extends Animal {
String speak() { return "meow"; }
}The output shows the runtime type winning every time. The reference a is declared as Animal but holds a Dog, so speak produces woof. The same method, called on a reference holding a Cat, produces meow. The helper method shows it is not an accident of the call site: it receives the reference as an Animal, and dispatch still reaches the dog's implementation, twice.
Two limits are worth stating. A private method is not inherited, so a subclass method with the same name is a separate method and no dispatch happens. A static method has no receiver, so the compiler resolves it from the declared type and there is no runtime lookup at all.
The cost is a small amount of work per call, which is why hot paths sometimes avoid megamorphic call sites where many different implementations flow through the same call. That is a genuine but second-order concern, and the design answer is still the same: express the variation in the type hierarchy rather than in branches at the call site.