🧬 Inheritance & Polymorphism · Intermediate

Dynamic dispatch in Java

The runtime object type decides which overridden method runs.

🧩 The mysteryOne line of code: shape.area(). Sometimes it computes a circle, sometimes a square, sometimes a triangle. Same line — the object decides. That's polymorphism.

Same call, different behavior

When you call an instance method, the JVM picks the implementation from the object's actual runtime class, not from the variable's declared type. This is *dynamic dispatch*.

Animal a = new Dog();
a.speak();  // runs Dog's speak()
šŸ”® Predict it

A loop full of birds

What does this print?

class Bird { String move() { return "fly"; } }
class Penguin extends Bird {
    String move() { return "waddle"; }
}
void main() {
    Bird[] bs = { new Penguin(), new Bird() };
    for (Bird b : bs)
        System.out.print(b.move() + " ");
}
  1. fly fly
  2. waddle fly
  3. waddle waddle
Show the answer

waddle fly. Both elements are typed Bird, but each call runs the move() of the actual object.

Two questions, two answers

For a.speak(): (1) Does it compile? The compiler checks that the declared type has speak(). (2) Which body runs? At runtime, the JVM looks up the override in the object's class.

āš ļø The trap

The declared type limits you

The object is a Dog, but the compiler only knows a is an Animal — and Animal has no fetch(). Compile error, before anything runs.

class Animal { }
class Dog extends Animal {
    void fetch() { }
}
Animal a = new Dog();
a.fetch();  // error!
šŸ”® Predict it

Calls inside the parent dispatch too

print() lives in Report. What does it show?

class Report {
    String title() { return "Report"; }
    void print() {
        System.out.println(title() + "!");
    }
}
class Sales extends Report {
    String title() { return "Sales"; }
}
void main() { new Sales().print(); }
  1. Report!
  2. Sales!
  3. Compile error
Show the answer

Sales!. print() is inherited from Report, but its call to title() is still dispatched on the real object — a Sales.

šŸ’¼ In the real world

In real projects

Dynamic dispatch replaces long if (type == ...) chains: put shapes, payment methods or notification channels in one list and call the same method on each. Frameworks call your overrides this way — you write run(), they call it.

Key takeaways

  1. Declared type decides what compiles
  2. Runtime class decides which override runs
  3. Calls inside the parent's own methods dispatch too
  4. Lets one loop handle many subtypes
🤯 Did you know?

The HotSpot JIT often makes polymorphic calls free: if only one implementation is actually used at a call site, it can inline that method directly.

Practice questions

What does this print?

class Animal { String speak() { return "?"; } }
class Dog extends Animal {
    String speak() { return "Woof"; }
}
void main() {
    Animal[] zoo = { new Dog(), new Animal() };
    for (Animal a : zoo)
        System.out.print(a.speak() + " ");
}
  1. ? ?
  2. Woof ?
  3. Woof Woof
  4. Compile error
Check your answer

Woof ?. Both elements are typed Animal, but each call runs the speak() of the actual object: first the Dog's, then plain Animal's.

What does this print?

class A {
    void run() { System.out.print(name()); }
    String name() { return "A"; }
}
class B extends A {
    String name() { return "B"; }
}
void main() { new B().run(); }
  1. A
  2. B
  3. AB
Check your answer

B. run() is inherited from A, but its call to name() is still dispatched on the real object, which is a B.

Next: overloading vs overriding — one is decided by the compiler, the other at runtime.