Polymorphism and Dynamic Dispatch
Polymorphism lets one call run type-specific code. Overriding binds at run time, overloading at compile time. Compiled Java output, vtables, a Python timing and the Liskov pitfalls.
Polymorphism lets the same method call run different code depending on the object's real type, so callers depend only on a shared interface and new types plug in without edits. Interviewers expect the binding rule: overriding is resolved at run time from the object's actual class, while overloading is resolved at compile time from the declared types of the arguments.
Polymorphism lets the same method call run different code depending on the object's type, so callers depend on an interface and new types plug in without edits. Overriding is resolved at run time from the object's real class, while overloading is resolved at compile time from declared types, and the compiled Java output below shows the difference.
- Method overriding is resolved at run time from the object's real class, while overloading is resolved at compile time from the declared types of the arguments.
- In the compiled Java example, print(s) chose print(Shape) for a Circle held in a Shape variable, while s.area() ran Circle's override and returned 12.57.
- Static methods in Java are hidden, not overridden, so s.kind() printed Shape for a Circle object because the declared type decides.
- A virtual method table holds one slot per virtual method for each class, and each object carries a hidden pointer to its class's table that a virtual call indexes.
- In a Python 3.13 timing over 10,000 shapes, polymorphic s.area() calls took about 0.78 ms and an if/elif type branch about 0.83 ms, within about 10 percent.
- The Liskov substitution principle requires a subclass used through its parent type to keep the parent's promises; Square extends Rectangle is the classic violation.
01One call, many behaviours
Polymorphism means one name, several forms. In object-oriented code the common form is subtype polymorphism: you hold a reference of a general type (Shape) and call a method (area), and the version that runs depends on the real object behind the reference (Circle or Square).
The payoff is that the calling code does not change when you add a new type. A loop that sums areas works for a Triangle class written next year. Without polymorphism the loop would contain an if/elif chain on the type, and every new type would force an edit in every such chain.
02Overriding is resolved at run time, overloading at compile time
These two words are mixed up in almost every interview. This Java program shows both, and I compiled and ran it with the JDK on this machine:
static class Shape {
double area() { return 0; }
String describe() { return "shape with area " + area(); }
static String kind() { return "Shape"; }
}
static class Circle extends Shape {
double r; Circle(double r) { this.r = r; }
@Override double area() { return Math.round(Math.PI * r * r * 100) / 100.0; }
static String kind() { return "Circle"; }
}
static class Square extends Shape {
double s; Square(double s) { this.s = s; }
@Override double area() { return s * s; }
}
static String print(Shape s) { return "print(Shape)"; }
static String print(Circle c) { return "print(Circle)"; }
// main
for (Shape s : List.of(new Circle(1), new Square(3), new Shape()))
System.out.println(s.getClass().getSimpleName() + ": " + s.describe());
Shape s = new Circle(2);
System.out.println(print(s)); // overload
System.out.println(print((Circle) s));
System.out.println(s.kind()); // static
System.out.println(s.area()); // overrideOutput:
Circle: shape with area 3.14
Square: shape with area 9.0
Shape: shape with area 0.0
print(Shape)
print(Circle)
Shape
12.57Read it line by line. describe() is defined once in Shape, yet it calls whichever area() belongs to the real object. print(s) chooses print(Shape) even though the object is a Circle, because overloads are picked from the declared type of the argument; the cast changes the declared type and changes the choice. s.kind() printed Shape because static methods are hidden, not overridden, so the declared type decides. Only s.area() used the real class: 12.57.
03How dynamic dispatch works underneath
The usual implementation, described for C++ on the dynamic dispatch Wikipedia page, is a virtual method table. Each class has a table with one slot per virtual method. Each object carries a hidden pointer to its class's table. A virtual call reads that pointer, indexes the slot, and jumps. A subclass copies its parent's table and replaces the slots it overrides.
Java's virtual method invocation follows the same idea and the Oracle tutorial describes it as the JVM choosing the method from the object's actual type; HotSpot may inline a call when it can prove or speculate which class it sees, which is why the cost is often near zero in practice. Python does not use vtables: it looks the name up through the class's method resolution order at call time. For Circle, Circle.__mro__ prints (Circle, Shape, object), and area is found on the first class in that list that defines it.
04Does polymorphism cost speed?
A common worry is that virtual calls are slower than a branch on type. I measured it in Python 3.13: 10,000 shapes (Circle and Square mixed), summing areas either by calling s.area() or by if type(s) is Circle ... elif type(s) is Square .... Best of five repeats, per 10,000 shapes: about 0.78 ms for the polymorphic version and 0.83 ms for the branching version. A separate earlier run gave 1.07 and 1.14 ms, so the absolute numbers move with the machine, but the two were within about 10 percent of each other both times.
This is Python on one machine, so it says little about Java or C++ hot loops. In C++ a virtual call can block inlining, which is where most of its cost comes from; in tight numeric loops that can matter, elsewhere it rarely does. The design benefit of adding a type without touching callers almost always outweighs it.
05Polymorphism beyond inheritance
Subtype polymorphism does not require a class hierarchy. In Java, a class can implement an interface and be used wherever that interface is expected, with no shared parent. In Python, duck typing means any object with an area() method works in the loop, whether or not it inherits from Shape. typing.Protocol lets type checkers verify that structurally.
Parametric polymorphism is different: a generic List<T> or a Python list[T] works for any element type with one implementation. Ad hoc polymorphism covers operator and function overloading, where the same name has separate implementations per argument type.
A frequent design error is building a deep hierarchy only to get polymorphism. The inheritance versus composition topic covers why a small interface plus composition often leads to code that is easier to change.
06Pitfalls
Overridable methods in constructors: in Java, a superclass constructor that calls an overridable method runs the subclass's version before the subclass's fields are initialised, so it may see null or zero. Avoid it, or make the method final.
Use @Override in Java so the compiler rejects a method that was meant to override but has a slightly different signature. Without it, you silently create an overload and the polymorphic call never reaches your code.
Liskov substitution: a subclass used through the parent type must keep the parent's promises. The classic example is Square extends Rectangle with a setWidth that also changes height. Code written for Rectangle then breaks. If a subclass has to weaken or surprise, the hierarchy is wrong.
07Common interview questions
Q: What is polymorphism?
A: Calling the same method on different types and getting type-specific behaviour, with the caller depending only on the shared interface.
Q: Overloading versus overriding?
A: Overloading is several methods with one name and different parameters, chosen at compile time from the declared types. Overriding replaces a parent's method in a subclass, chosen at run time from the object's real class.
Q: Can static methods be overridden?
A: No. They are hidden, and the declared type decides which runs, as the s.kind() output of Shape above shows.
Q: How does a virtual call work?
A: The object points to its class's vtable; the call indexes the method's slot and jumps to the stored address.
Q: Does Python have overloading?
A: Not by signature; a later def with the same name replaces the earlier one. Use default arguments, functools.singledispatch, or *args.
Q: What is the Liskov substitution principle?
A: Anywhere a parent type is used, a subclass instance must work without breaking expectations.