Command Palette

Search for a command to run...

PHASE 5Intermediate ~31 min· topic 4 of 13

Topic 5.4

Polymorphism and Dynamic Dispatch

In one line

Polymorphism means one variable type can hold objects of many classes, and a method call runs the version that belongs to the actual object. The JVM does this with dynamic dispatch, using per-class method tables (vtables) that the JIT then optimises away when it can.

Think of it like this

A TV remote's power button. The same button works on a Sony, a Samsung or an LG TV. You press 'power' without knowing the brand, and each TV turns on in its own way. The button is the method call; each TV brand is a different object; the TV decides what 'power on' means. That is polymorphism: one call, many behaviours.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Polymorphism
Greek for 'many forms'. In Java: one type (like Shape) can refer to objects of many classes, and each responds to calls in its own way.
Declared type
The type written for a variable or expression in the source code. The compiler checks calls against it. Also called the static or compile-time type.
Runtime class
The actual class of the object in memory, the one used with new. getClass() returns it.
Dynamic dispatch
Choosing which method body to run at run time, based on the object's runtime class. Also called late binding.
vtable
Virtual method table: a per-class array of pointers to the method code for each overridable method, used to find the right override quickly.
invokevirtual
The bytecode instruction for a normal instance-method call on a class type. It triggers dynamic dispatch.
Inlining
A JIT optimisation that copies a small method's body into the caller, removing the call entirely.
Open/closed principle
Code should be open to new behaviour (add a subclass) without modifying existing code (the loop that uses it).

Step by step

01A parent-type variable holding a child object

Shape s = new Circle(1); is legal because every Circle is a Shape. Nothing is converted or copied: s simply holds a reference to the Circle object.

From now on, through s, you can only use what Shape declares. The object is still fully a Circle; you've just chosen to view it through a narrower window.

A parent-type variable holding a child objectdiagram
Rendering diagram…

02The compiler checks, the JVM chooses

Step 1, compile time: javac sees s.area(). Does Shape have an area() method? Yes, so it compiles, and javac writes invokevirtual Shape.area() into the bytecode. It doesn't know (or care) which subclass will be there.

Step 2, run time: the JVM executes invokevirtual. It looks at the object on the stack, finds its class is Circle, and runs Circle.area().

If you wrote s.radius(), step 1 fails: Shape has no radius(), so cannot find symbol. The compiler won't guess what's in s at run time.

terminal
$ javac Main.java
javap -c Main
── expected output ──
public static void main(java.lang.String[]);
Code:
0: new #11 // class Dog
3: dup
4: invokespecial #13 // Method Dog."<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #14 // Method Animal.sound:()Ljava/lang/String;
12: astore_2
13: new #20 // class English
16: dup
17: invokespecial #22 // Method English."<init>":()V
20: astore_3
21: aload_3
22: invokeinterface #23, 1 // InterfaceMethod Greeter.greet:()Ljava/lang/String;
27: pop
28: aload_2
29: invokestatic #28 // Method twice:(Ljava/lang/String;)Ljava/lang/String;
32: pop
33: return
The source was `Animal a = new Dog(); String s = a.sound(); Greeter g = new English(); g.greet(); twice(s);`. The bytecode names Animal.sound, not Dog.sound: the real target is found at run time.

03The four call instructions

invokevirtual: instance methods on a class type. Dynamically dispatched.

invokeinterface: instance methods on an interface type. Dynamically dispatched, but the method can be in a different slot in each class, so it needs an extra search (itables).

invokespecial: constructors, super.m() calls, and (traditionally) private methods. Exact target, no dispatch.

invokestatic: static methods. Exact target, no object at all. (A fifth, invokedynamic, powers lambdas and string concatenation; Phase 10.)

04Inside the JVM: vtables

When HotSpot loads a class, it builds its vtable. Slot numbers are inherited: if Shape.area() is slot 5, then area() is slot 5 in Circle, Rect and Square too. Circle's slot 5 points to Circle's code; Square, which doesn't override area(), keeps Rect's pointer in slot 5.

So invokevirtual Shape.area() becomes, after linking: read the object's class pointer from its header, read vtable slot 5 from that class, jump to that address. A constant number of steps, no searching by name.

Interface methods can't use a fixed slot (a class can implement many interfaces in any order), so each class also has itables, per-interface tables found by a short search, which is why invokeinterface used to be slightly slower before the JIT optimises it.

Inside the JVM: vtablesdiagram
Rendering diagram…

05What the JIT does with a virtual call

A vtable lookup is cheap, but the real cost of a virtual call is that the JIT can't inline it if it doesn't know the target. HotSpot works around this in two ways.

Class hierarchy analysis (CHA): if only one loaded class implements area(), the call has one possible target, so the JIT inlines it directly. If a new subclass is loaded later, the JVM deoptimises the affected code and recompiles.

Inline caches: at each call site, the JVM records which classes it actually sees. Seen one class (*monomorphic*)? Inline it behind a quick class check. Two (*bimorphic*)? Inline both. Three or more (*megamorphic*)? Fall back to the vtable/itable call. So a loop over many shape types is slower per call than a loop over one type, which matters in hot code.

06Fields and static methods don't take part

Dynamic dispatch applies to instance methods only. If A and B extends A both declare a field name, then A ref = new B(); ref.name reads A's field. If both declare static kind(), ref.kind() calls A's. The compiler binds these by the declared type.

This is another reason to keep fields private and expose behaviour through methods: methods are polymorphic, fields aren't.

07Calls from inside the parent are virtual too

When a parent method calls another overridable method (return greeting() + ", " + n;), that inner call is also invokevirtual on this. If the object is a subclass that overrides greeting(), the subclass's version runs. This is the engine behind the Template Method pattern (Topic 5.6), and also behind the constructor trap from Topic 5.2.

Try it yourself

  1. 1

    Add a new shape without touching the loop

    In the shapes example, add class Triangle extends Shape with base and height, overriding area() (0.5 × b × h) and name(). Add new Triangle(4, 3) to the array only. Predict the new total (19.14) and run.

  2. 2

    Watch the compiler refuse

    In the shapes example, inside the loop write System.out.println(s.r);. Read the cannot find symbol error: the compiler only knows s is a Shape. Then guard it with if (s instanceof Circle c) System.out.println(c.r); (Topic 5.5).

  3. 3

    Look at the bytecode yourself

    If you have a JDK installed locally, compile the shapes example and run javap -c Main. Find the invokevirtual lines for name() and area(); both name Shape, never Circle.

    terminal
    $ javac Main.java
    javap -c Main | findstr invokevirtual
    ── expected output ──
    ...invokevirtual #NN // Method Shape.name:()Ljava/lang/String;
    ...invokevirtual #NN // Method Shape.area:()D
    ...
    Use grep instead of findstr on macOS/Linux. Constant-pool numbers vary.

Code & diagrams

One loop, many shapes New tab

The loop only knows about Shape. Add a Triangle class and the loop works unchanged.

Sign in to run this example in your browser.

Expected output

circle area = 3.14
rect   area = 6.00
square area = 4.00
total = 13.14
Methods are polymorphic, fields and statics are not New tab
Sign in to run this example in your browser.

Expected output

field:  A-field
static: A-static
method: B-method
class:  B
cast field: B-field
A parent method calling an overridden method New tab

PirateGreeter changed one small step; Greeter's greet() picked it up automatically.

Sign in to run this example in your browser.

Expected output

Greeter: Hello, Sam!
PirateGreeter: Ahoy, Sam!
ShyGreeter: ...hi Sam

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Call a subclass-only method through a parent reference

Store a Dog in an Animal variable and call fetch(), which only Dog has.

Main.javawhole filejava
class Animal { void speak() { } }
class Dog extends Animal { void fetch() { } }
public class Main { public static void main(String[] a) { Animal a1 = new Dog(); a1.fetch(); } }
terminal
$ javac Main.java
── what you'll see ──
Main.java:3: error: cannot find symbol
public class Main { public static void main(String[] a) { Animal a1 = new Dog(); a1.fetch(); } }
^
symbol: method fetch()
location: variable a1 of type Animal
1 error

Myth vs fact

Myth

Animal a = new Dog() turns the Dog into an Animal.

Fact

The object stays a Dog forever. Only your view of it, the reference's declared type, is Animal. a.getClass() still says Dog.

Myth

Virtual calls are slow, so mark everything final for speed.

Fact

HotSpot inlines monomorphic and bimorphic call sites using profiling and class hierarchy analysis, final or not. final is a design tool, not a performance switch. Megamorphic sites in very hot loops are the case that actually costs.

Myth

Polymorphism means overloading.

Fact

Overloading is sometimes called 'compile-time polymorphism', but when Java developers say polymorphism they mean subtype polymorphism with dynamic dispatch. Overloads are resolved by the compiler once.

Interview problem

The problem

What does this print?

An interviewer shows you: class A { String f = "A"; String m() { return "A"; } String show() { return f + m(); } } and class B extends A { String f = "B"; @Override String m() { return "B"; } }, then A x = new B(); System.out.println(x.f + x.m() + x.show());. What's printed, and why?

The interviewer follows up

01

What if m() were private in both classes?

02

What if m() were static in both?

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    vtable layout: in HotSpot the vtable lives at the end of the Klass structure. Object's virtual methods (equals, hashCode, toString, clone, finalize) occupy the first slots of every class's vtable. final methods that don't override anything get no vtable slot at all; calls to them are bound directly.

  • ▸

    invokeinterface dispatch searches the receiver class's itable for the interface, then indexes into it. HotSpot's inline caches make the common monomorphic case as fast as a class-typed call; the measurable difference only appears at megamorphic sites. Profile with -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining (Topic 14.4) to see 'monomorphic', 'bimorphic' or 'virtual call' decisions.

  • ▸

    A classic performance trick is to keep hot call sites monomorphic: for example, avoid passing four different Comparator lambda classes through one shared sorting helper in a tight loop. Each new receiver class seen at a site can push it from inlined to megamorphic.

  • ▸

    Polymorphism is how the GoF patterns work: Strategy swaps an algorithm object behind an interface, State swaps behaviour by changing which object handles calls. See the System Design course's Strategy (/topic/phase-2/strategy) and State (/topic/phase-2/state) pattern pages.

Remember this

  1. 1

    A variable has a declared type (also called static or compile-time type), written in the source: Shape s. The object it points to has a runtime class: new Circle(1). Because a Circle *is a* Shape, the assignment is allowed. This is the heart of subtype polymorphism.

  2. 2

    The compiler uses the declared type to decide what you're *allowed* to call. If Shape has no radius() method, s.radius() fails to compile even when s holds a Circle. The JVM uses the runtime class to decide *which code runs*. s.area() runs Circle's area(). Compile-time checks, run-time choice.

  3. 3

    This run-time choice is dynamic dispatch (or late binding). The bytecode for s.area() is invokevirtual Shape.area(); at run time the JVM looks in the object's class for the most specific area(). Calls through an interface type use invokeinterface, which works the same way.

  4. 4

    Only instance methods are dispatched dynamically. Fields are chosen by the declared type, static methods by the declared type, private methods and constructors are called directly, and overloads are chosen by argument types at compile time (Topic 5.3).

  5. 5

    Why it matters: code written against the parent type works with every subclass, including ones written later. A loop over List<Shape> that calls area() never changes when you add Triangle. This is the open/closed principle: open for extension, closed for modification.

  6. 6

    Under the hood, HotSpot gives each class a vtable, an array of method pointers. A subclass copies its parent's table and replaces the entries it overrides, so area() sits at the same slot index in every Shape subclass. A virtual call is then: load the object's class, load slot N, jump. The JIT usually does even better by inlining the likely target.

Explain it without notes

01

Explain the difference between a variable's declared type and its object's runtime class, and what each one controls.

02

What is dynamic dispatch and which bytecode instructions perform it?

03

How does a vtable make virtual calls fast?

04

Which members are NOT polymorphic in Java?

05

Why can a megamorphic call site be slower than a monomorphic one?

Practice

01

Model Employee with pay() returning a base salary, and subclasses Intern (half pay) and Contractor (hourly × hours). Compute the total payroll for a mixed list.

02

Predict, then verify: class P { void hi() { System.out.println("P"); } void run() { hi(); } }, class Q extends P { void hi() { System.out.println("Q"); } }, calling new Q().run() and new P().run().

03

Write a Notifier base class with send(String) and two subclasses. Write a static method broadcast(List<Notifier>, String) that never mentions the subclasses.

Trade-offs

  • ↔

    Polymorphism moves the 'which kind is it?' decision out of if/switch chains into the class hierarchy. Adding a new kind is easy (a new subclass); adding a new *operation* to every kind means touching every class. Pattern matching over sealed types (Topic 5.11) flips that trade-off.

  • ↔

    Programming to the parent type makes code flexible, but readers must look in several classes to know what a call does. Keep hierarchies shallow and override sparingly.

  • ↔

    In very hot code, megamorphic call sites cost real time. Usually irrelevant; measure before restructuring.

Done when you can

  • Done when you can explain declared type vs runtime class and what each controls.

  • Done when you can predict output involving fields, static methods and overridden methods through a parent reference.

  • Done when you can name the four invoke instructions and which ones dispatch.

  • Done when you can explain vtables and inline caches at an interview level.

  • Done when you can write a loop over a parent type that works for any future subclass.