Polymorphism: Compile-time vs Runtime โ Revision Notes
Polymorphism โ "many forms" โ is the fourth OOP pillar and the concept interviewers probe most for depth. The compile-time vs runtime split, and how runtime dispatch actually works (virtual tables), separate strong candidates.
Two kinds
- Compile-time (static) polymorphism: resolved by the compiler via method overloading (same name, different parameter lists) and operator overloading. Also called early binding.
- Runtime (dynamic) polymorphism: resolved at execution via method overriding โ a subclass redefines a base method; the actual object's type decides which runs. Also called late binding.
| Compile-time | Runtime | |
|---|---|---|
| Mechanism | overloading | overriding |
| Binding | early (compiler) | late (runtime) |
| Needs | โ | inheritance + virtual/override |
How runtime dispatch works
The base method must be virtual (C++) / non-final (Java, virtual by default). The compiler builds a vtable (virtual method table) per class; each object holds a pointer to it, so a call is dispatched to the correct override based on the actual object type, not the reference type.
Exam Tricks & Tips
- ๐ฏ Overloading = compile-time, overriding = runtime โ the single most important distinction to state clearly.
- ๐ฏ Runtime polymorphism needs inheritance + an overridden method; in C++ the base method must be
virtual, else you get static binding. - ๐ฏ A base-class reference to a derived object calls the derived override at runtime (dynamic dispatch) โ the reference type doesn't decide.
- ๐ฏ The vtable (virtual table) is the mechanism: one per class, holding pointers to the correct method versions.
- ๐ฏ In Java, methods are virtual by default;
final/static/privatemethods are not overridable (statically bound). - ๐ฏ Static methods are hidden, not overridden โ resolved by reference type at compile time.
- โ Common mistake: expecting a
staticor non-virtual method call to dispatch dynamically โ it binds by the reference/declared type.
Expected pattern
"Compile-time vs runtime polymorphism", "how does dynamic dispatch/vtable work?", "overloading vs overriding", "what does virtual do?", and predict-the-output with base references to derived objects.
Quick recap
Polymorphism = many forms. Compile-time (overloading, early binding) vs runtime (overriding, late binding). Runtime needs inheritance + virtual/overridable methods; dispatch via per-class vtable using the object's actual type. Java methods are virtual by default; static/final/private are statically bound.
Polymorphism: Compile-time vs Runtime โ Flashcards
Cover the answer, recall, then check. 12 cards on polymorphism.
Q1. What does polymorphism mean?
A1. "Many forms" โ the same interface/call behaving differently depending on the actual type or arguments.
Q2. Which mechanism gives compile-time polymorphism?
A2. Method (and operator) overloading โ resolved by the compiler based on the parameter list (early binding).
Q3. Which mechanism gives runtime polymorphism?
A3. Method overriding โ a subclass redefines a base method; the object's actual type decides which runs (late binding).
Q4. What is required for runtime polymorphism?
A4. Inheritance plus an overridden (virtual/overridable) method, called through a base-type reference.
Q5. What is a vtable?
A5. A virtual method table โ one per class holding pointers to the correct method implementations, used for dynamic dispatch.
Q6. With a base reference to a derived object, which method runs?
A6. The derived class's override (runtime dispatch) โ the actual object type, not the reference type, decides.
Q7. What does the virtual keyword do in C++?
A7. Marks a method for dynamic dispatch, enabling overriding; without it the call is statically bound.
Q8. Are Java methods virtual by default?
A8. Yes โ except static, final, and private methods, which are statically bound and not overridable.
Q9. Overloading vs overriding โ the core difference?
A9. Overloading: same name, different params, compile-time. Overriding: same signature in a subclass, runtime.
Q10. Can static methods be overridden?
A10. No โ they are hidden, resolved by the reference/declared type at compile time, not dynamically dispatched.
Q11. What is early vs late binding?
A11. Early binding resolves the call at compile time (overloading); late binding resolves it at runtime (overriding).
Q12. Why is dynamic dispatch slightly slower than a direct call?
A12. It requires a vtable lookup to find the correct method at runtime, adding a small indirection.
Polymorphism: Compile-time vs Runtime
Polymorphism โ "many forms" โ is what lets one interface drive many behaviours. Distinguishing its two flavours (compile-time vs runtime) and knowing the mechanism behind each is a guaranteed interview topic.
Core idea: polymorphism lets the same call take different forms depending on context. Compile-time (static) polymorphism is resolved by the compiler โ method overloading and generics. Runtime (dynamic) polymorphism is resolved during execution by the object's actual type โ method overriding, dispatched through a virtual table.
How it works
Beginner โ the two kinds
// Compile-time: overloading (same name, different params) -> compiler picks
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
// Runtime: overriding (subclass redefines) -> object's real type picks
class Animal { void sound() { print("..."); } }
class Dog extends Animal { void sound() { print("woof"); } }
Intermediate โ dynamic dispatch
The heart of runtime polymorphism:
Animal a = new Dog(); // reference type Animal, actual object Dog
a.sound(); // prints "woof" -- the DOG's version runs
Even though a is declared Animal, the call dispatches to Dog.sound() because the JVM looks up the method on the actual object, not the reference type. This "upcast the reference, dispatch on the object" pattern is how you write code against a base type that transparently runs specialised behaviour.
Advanced โ the vtable mechanism
Runtime dispatch is implemented with a virtual method table (vtable): each class has a table of function pointers to its method implementations, and each object carries a hidden pointer to its class's vtable. A virtual call is "look up slot k in the object's vtable and jump there" โ one extra indirection. In C++ methods are dispatched statically unless marked virtual; in Java instance methods are virtual by default (use final/static to make them non-virtual).
Animal* a = new Dog();
a->sound(); // C++: virtual? -> Dog::sound via vtable; non-virtual -> Animal::sound
Worked example โ polymorphic loop
Animal[] zoo = { new Dog(), new Cat(), new Cow() };
for (Animal x : zoo)
x.sound(); // woof / meow / moo -- each dispatches to its own type
One loop, three behaviours, and no if/instanceof chain. Adding a new Sheep subclass requires zero changes to this loop โ it just works. That extensibility (the Open/Closed Principle in action) is the entire point of runtime polymorphism.
Interview & exam relevance
Core asks: "compile-time vs runtime polymorphism" (overloading vs overriding), "how does dynamic dispatch / vtable work", "output of an upcast call" (Animal a = new Dog(); a.sound()), and in C++ "what does virtual do / why do you need a virtual destructor?" A missing virtual destructor in C++ leaks when deleting a derived object via a base pointer.
Tricks & gotchas
- Overloading is chosen by the reference/argument types at compile time; overriding by the object's real type at runtime.
- Fields are not polymorphic โ access to a field uses the reference type, not the object's type (a common trap).
- In C++, forgetting
virtualmeans a base-pointer call runs the base version (static binding). - Mnemonic: "Overload = compiler chooses; override = object chooses."
Expecting field access or static methods to be polymorphic. Only overridden instance methods are dispatched on the object's real type; fields and static methods bind to the reference (declared) type. So Animal a = new Dog(); a.legs reads Animal.legs, not Dog.legs โ a subtle but frequently tested distinction.
- โ- Compile-time polymorphism = overloading/generics, resolved by the compiler.
- โ- Runtime polymorphism = overriding, resolved by the object's actual type.
- โ- Dynamic dispatch uses a per-class vtable of method pointers (one indirection).
- โ- Upcasting a reference still runs the derived override (
Animal a = new Dog()). - โ- Fields and static methods bind to the reference type, not the object type.
- โPolymorphism gives one interface many behaviours. Overloading is picked by the compiler from argument types; overriding is dispatched at runtime on the object's real type via a vtable, letting base-typed code transparently run specialised subclass behaviour and stay open to extension.
Polymorphism: Compile-time vs Runtime โ Worked Example
Worked Example
Problem: Predict the output and explain which calls are resolved at compile time and which at runtime.
class Animal {
String speak() { return "..."; }
static String kind() { return "Animal"; } // static
}
class Dog extends Animal {
String speak() { return "Woof"; } // override (runtime)
static String kind() { return "Dog"; } // hides (compile-time)
String speak(int times) { return "Woof x" + times; } // overload
}
public class Demo {
public static void main(String[] a) {
Animal x = new Dog(); // static type Animal, dynamic type Dog
System.out.println(x.speak()); // (A)
System.out.println(x.kind()); // (B)
Dog d = new Dog();
System.out.println(d.speak(3));// (C)
}
}
Solution:
Two kinds of polymorphism are in play.
Runtime (dynamic) polymorphism = method overriding. The actual method is chosen from the object's runtime type via dynamic dispatch (virtual method table), regardless of the reference's declared type.
(A) x is declared Animal but points to a Dog; x.speak() dispatches on the Dog object โ "Woof".
Static methods are NOT polymorphic โ they are hidden, not overridden, and resolved by the reference's compile-time type.
(B) x has static type Animal, so x.kind() binds to Animal.kind() at compile time โ "Animal" (a classic trap; it does NOT print "Dog").
Compile-time (static) polymorphism = method overloading, resolved by the argument types the compiler sees.
(C) d.speak(3) matches the overload speak(int) โ "Woof x3", chosen at compile time by the argument list.
So overriding is decided by the object at run time; overloading and static-method calls are decided by the declared type at compile time.
Answer: (A) "Woof" (runtime override dispatches on the Dog object), (B) "Animal" (static methods bind to the compile-time type, they are hidden not overridden), (C) "Woof x3" (overload resolved at compile time by argument type).
- โ- Overriding = runtime polymorphism: resolved by the object's actual type via dynamic dispatch.
- โ- Overloading = compile-time polymorphism: resolved by argument types the compiler sees.
- โ- static (and field) accesses bind to the reference's declared type, so they are not overridden but hidden.