Runtime Polymorphism and Dynamic Method Dispatch

Naveen AnamaneniUpdated · 6 min read

In Shape s = ...; s.area(); the compiler doesn't know which shape s will hold: a circle, a rectangle or a square might come from user input. So for overridden methods, the final choice is left to the JVM, at runtime: it looks at the actual object, finds the right area() for its class, and runs that. This mechanism is called dynamic method dispatch, and it's what makes runtime polymorphism work.

One Loop, Mixed Shapes

Shapes.java▶ Try it
public class Shapes {
    static class Shape {
        double area() { return 0; }
        String name() { return "Shape"; }
    }
 
    static class Circle extends Shape {
        double r;
        Circle(double r) { this.r = r; }
        @Override double area() { return Math.PI * r * r; }
        @Override String name() { return "Circle"; }
    }
 
    static class Rectangle extends Shape {
        double w, h;
        Rectangle(double w, double h) { this.w = w; this.h = h; }
        @Override double area() { return w * h; }
        @Override String name() { return "Rectangle"; }
    }
 
    static class Square extends Rectangle {
        Square(double side) { super(side, side); }
        @Override String name() { return "Square"; }
    }
 
    public static void main(String[] args) {
        Shape[] shapes = { new Circle(1), new Rectangle(2, 3), new Square(4) };
        double total = 0;
        for (Shape s : shapes) {
            System.out.printf("%-9s %6.2f%n", s.name(), s.area());
            total += s.area();
        }
        System.out.printf("%-9s %6.2f%n", "Total", total);
    }
}
Output
Circle      3.14
Rectangle   6.00
Square     16.00
Total      25.14

How it works

  1. s is declared as Shape, but holds a Circle, then a Rectangle, then a Square.
  2. For each s.area(), the JVM checks the object's class and runs that class's area().
  3. Square doesn't declare area(). The JVM walks up to its parent, Rectangle, and runs Rectangle.area(): 4 × 4 = 16.
  4. Square does declare name(), so its own version runs.

Try it yourself: add a Triangle with a base and height. The loop in main needs no change.

The Lookup, Step by Step

Start at the object's class, walk up until the method is found

The rule the JVM follows:

  1. Start at the actual class of the object (not the variable's type).
  2. If that class declares the method, run it.
  3. Otherwise move to its parent and check again, up to Object.

The compiler has already guaranteed the search will succeed: s.area() compiled only because Shape (the declared type) has an area() method.

What Is and Isn't Dispatched Dynamically

MemberChosen byDynamic?
Overridable instance methodsThe object's class, at runtimeYes
static methodsThe declared type, at compile timeNo
private methodsThe declaring classNo
final methodsOnly one version existsNo (nothing to choose)
FieldsThe declared typeNo
OverloadsThe compiler picks the signatureNo (the signature is fixed; the implementation of it can still be dispatched)

Common Mistakes

Thinking the variable's type picks the method. It only decides what you may call.

Calling an overridable method from a constructor. The child's override runs before the child's fields are set ("Order of Initialization" showed the order), so it can see zeros and nulls.

Expecting fields to be dispatched. s.r doesn't compile through a Shape variable, and redeclared fields follow the declared type.

Under the Hood

The compiler writes an invokevirtual instruction that names the declared type's method:

javap -c Shapes (trimmed)
        85: invokevirtual #36                 // Method Shapes$Shape.name:()Ljava/lang/String;
        93: invokevirtual #40                 // Method Shapes$Shape.area:()D

The bytecode says Shape.area, yet Circle.area runs. At runtime, invokevirtual uses the object's class to find the actual method. To keep that fast, HotSpot gives each class a method table (a "vtable"): an array of the class's overridable methods, where an overriding method takes the same slot as the method it overrides. area() sits in the same slot for Shape, Circle, Rectangle and Square; for Square, that slot simply points at Rectangle.area. A call becomes "read the object's class, read slot N, jump", with no searching.

The JIT compiler goes further. If a call site has only ever seen one or two classes, it inlines those methods directly with a quick type check. So virtual calls in Java are rarely slower than direct calls in practice.

Interview Corner

What is dynamic method dispatch?

It's the mechanism by which a call to an overridden method is resolved at runtime, using the class of the actual object rather than the type of the reference. The JVM starts at the object's class and uses the most specific overriding method. It's how Java implements runtime polymorphism: Shape s = new Circle(); s.area(); runs Circle.area().

How does the JVM implement dynamic dispatch efficiently?

invokevirtual names the method on the declared type. HotSpot keeps a virtual method table per class, where overriding methods occupy the same slot as the methods they override, so a call reads the slot from the object's class instead of searching. The JIT also inlines call sites that only see one or two receiver classes, guarded by a type check.

Practice

  1. Easy: Give Shape a method describe() that returns name() + " with area " + area(), without overriding it in any subclass. What does it print for a square, and why?
  2. Harder: Add a Cube class that extends Square and overrides area() to return the surface area (6 × side²). Predict all four lines before running.

Quick check: Dynamic dispatch

4 questions
1Shape s = new Square(4); s.area(); where only Rectangle declares area(). Which runs?
2What decides whether s.area() compiles?
3Which bytecode instruction calls an overridable instance method?
4Which is dispatched dynamically?

Key Takeaways

Was this tutorial helpful?

Lesson 129 of 482