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.
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.
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.
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
Add a new shape without touching the loop
In the shapes example, add
class Triangle extends Shapewith base and height, overridingarea()(0.5 × b × h) andname(). Addnew Triangle(4, 3)to the array only. Predict the new total (19.14) and run. - 2
Watch the compiler refuse
In the shapes example, inside the loop write
System.out.println(s.r);. Read thecannot find symbolerror: the compiler only knowssis a Shape. Then guard it withif (s instanceof Circle c) System.out.println(c.r);(Topic 5.5). - 3
Look at the bytecode yourself
If you have a JDK installed locally, compile the shapes example and run
javap -c Main. Find theinvokevirtuallines forname()andarea(); both nameShape, neverCircle.terminal$ javac Main.javajavap -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
The loop only knows about Shape. Add a Triangle class and the loop works unchanged.
Expected output
circle area = 3.14
rect area = 6.00
square area = 4.00
total = 13.14Expected output
field: A-field
static: A-static
method: B-method
class: B
cast field: B-fieldPirateGreeter changed one small step; Greeter's greet() picked it up automatically.
Expected output
Greeter: Hello, Sam!
PirateGreeter: Ahoy, Sam!
ShyGreeter: ...hi SamBreak 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.
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(); } }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
What if m() were private in both classes?
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
Klassstructure.Object's virtual methods (equals,hashCode,toString,clone,finalize) occupy the first slots of every class's vtable.finalmethods that don't override anything get no vtable slot at all; calls to them are bound directly. - ▸
invokeinterfacedispatch 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
Comparatorlambda 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
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
The compiler uses the declared type to decide what you're *allowed* to call. If
Shapehas noradius()method,s.radius()fails to compile even whensholds a Circle. The JVM uses the runtime class to decide *which code runs*.s.area()runs Circle'sarea(). Compile-time checks, run-time choice. - 3
This run-time choice is dynamic dispatch (or late binding). The bytecode for
s.area()isinvokevirtual Shape.area(); at run time the JVM looks in the object's class for the most specificarea(). Calls through an interface type useinvokeinterface, which works the same way. - 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
Why it matters: code written against the parent type works with every subclass, including ones written later. A loop over
List<Shape>that callsarea()never changes when you addTriangle. This is the open/closed principle: open for extension, closed for modification. - 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
Explain the difference between a variable's declared type and its object's runtime class, and what each one controls.
What is dynamic dispatch and which bytecode instructions perform it?
How does a vtable make virtual calls fast?
Which members are NOT polymorphic in Java?
Why can a megamorphic call site be slower than a monomorphic one?
Practice
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.
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().
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/switchchains 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.