Runtime Polymorphism, Virtual Methods and Dynamic Dispatch
Polymorphism is the pillar with the most machinery behind it. Knowing how the call is resolved — and what it costs — separates a memorised definition from an answer that holds up under follow-up questions.
How the call is resolved
When a method can be overridden, the compiler cannot know at compile time which implementation will run — the variable's declared type says Notification, but the object might be an SmsNotification. So the decision is deferred to runtime: each class has a table of method pointers (a vtable), each object knows its class, and the call is one extra indirection through that table.
- Java: every non-
final, non-private, non-staticmethod is virtual by default. - C++: only methods marked
virtualare dispatched dynamically; the rest are resolved at compile time. - Python and JavaScript: methods are looked up on the object at call time, so everything is effectively virtual.
Is it slow?
One pointer indirection, and the JIT often removes it entirely by inlining when only one implementation is ever loaded. Dispatch cost is almost never the reason a program is slow — do not design around it.
Abstract class or interface?
| Abstract class | Interface | |
|---|---|---|
| Instance fields | Yes | No (constants only) |
| Constructor | Yes | No |
| Method bodies | Yes | Yes, via default |
| How many per class | One | Many |
| Use when | Subclasses share state and behaviour | You are defining a capability |
The practical rule: reach for an interface first, because a class can implement several and it keeps the inheritance slot free. Move to an abstract class only when there is genuine shared state to hold.
Replacing conditional chains
The most valuable everyday use of polymorphism is deleting switch statements that grow every time a case is added.
double area(Shape s) {
if (s.type == CIRCLE) return Math.PI * s.r * s.r;
if (s.type == SQUARE) return s.side * s.side;
if (s.type == RECTANGLE) return s.w * s.h;
throw new IllegalArgumentException("unknown shape");
}
interface Shape { double area(); }
record Circle(double r) implements Shape { public double area() { return Math.PI * r * r; } }
record Square(double side) implements Shape { public double area() { return side * side; } }
record Rectangle(double w, double h) implements Shape { public double area() { return w * h; } }
This is the open/closed principle in one diff: the system is open to new shapes and closed to modification of existing code. The unreachable "unknown shape" branch disappears too, because the type system now guarantees it.
Key takeaways
- Dynamic dispatch defers the choice of implementation to runtime via a vtable lookup.
- Java methods are virtual by default; in C++ you opt in with the virtual keyword.
- Prefer interfaces; use an abstract class when subclasses genuinely share state.
- Replacing a type switch with polymorphism is the open/closed principle in practice.
Frequently asked questions
What is dynamic dispatch?
The mechanism that picks a method implementation at runtime based on the actual type of the object, rather than the declared type of the variable, usually through a per-class table of method pointers.
What is the difference between an abstract class and an interface?
An abstract class can hold instance state, a constructor and shared implementation, and a class can extend only one. An interface defines a capability, holds no instance state, and a class can implement many.
Can constructors be polymorphic?
No. Constructors are not inherited and are not dispatched dynamically. A factory method or the factory pattern is the usual way to choose which type to create at runtime.
Test yourself on OOPs Concepts
Reading is not recall. Take a timed quiz on this topic solo, or share a room code and battle friends on it.
⚡ Start the OOPs Concepts quiz