Polymorphism
Method overloading, method overriding, dynamic dispatch, covariant return types, vtable/itable and JIT optimization
Polymorphism
Polymorphism (Greek: "many forms") is one of the cornerstones of OOP: it allows a single interface or reference type to represent different behaviors. Its two main forms are compile-time (static) polymorphism and runtime (dynamic) polymorphism.
1. Definition
What is it?
- Polymorphism â the ability of an object to take on many forms. The same method call can execute different implementations depending on the actual runtime type of the object.
- Compile-time polymorphism (static): The compiler resolves which method to call â achieved via method overloading.
- Runtime polymorphism (dynamic): The JVM resolves which implementation to execute at runtime â achieved via method overriding and dynamic dispatch.
Why does it matter?
- Open/Closed Principle: New behaviors can be added without modifying existing code.
- Maintainability: Calling code does not depend on concrete implementations.
- Testability: Mock objects can easily substitute real implementations.
Where does it fit?
Among the four OOP pillars (encapsulation, inheritance, polymorphism, abstraction), polymorphism is the most practically pervasive â virtually every design pattern and framework relies on it.
2. Core Concepts
2.1 Method Overloading (compile-time / static polymorphism)
Multiple methods with the same name but different parameter lists (type, count, order) can coexist within a single class:
public class Calculator {
public int add(int a, int b) { return a + b; }
public double add(double a, double b) { return a + b; }
public int add(int a, int b, int c) { return a + b + c; }
}
The compiler selects the method at compile time based on the method signature. The return type alone is not sufficient to distinguish overloaded methods.
2.2 Method Overriding (runtime / dynamic polymorphism)
A subclass overrides a superclass method with the same signature, and at runtime the actual type of the object determines which implementation runs:
public class Animal {
public String speak() { return "..."; }
}
public class Dog extends Animal {
@Override
public String speak() { return "Woof!"; }
}
public class Cat extends Animal {
@Override
public String speak() { return "Meow!"; }
}
2.3 Dynamic Dispatch
The JVM looks up the correct method at runtime based on the actual (runtime) type of the reference:
Animal animal = new Dog(); // compile-time type: Animal, runtime type: Dog
animal.speak(); // â "Woof!" (dynamic dispatch)
The compiler verifies the method exists on the compile-time type (Animal), but the runtime type (Dog) implementation is invoked.
2.4 Covariant Return Types
Since Java 5, an overriding method can return a subtype of the original return type:
public class AnimalFactory {
public Animal create() { return new Animal(); }
}
public class DogFactory extends AnimalFactory {
@Override
public Dog create() { return new Dog(); } // Dog is a subtype of Animal â OK
}
2.5 Polymorphic References
A superclass or interface reference can point to any subclass/implementor instance:
List<Animal> animals = List.of(new Dog(), new Cat());
for (Animal a : animals) {
System.out.println(a.speak()); // each executes its own implementation
}
2.6 Varargs Overloading
Varargs (...) parameters are treated specially during overload resolution â the compiler prefers the most specific method:
public void print(int a, int b) { /* specific */ }
public void print(int... numbers) { /* varargs */ }
print(1, 2); // â the first method is called (more specific)
2.7 Type Erasure Impact on Overloading
Generic type parameters are erased at compile time (type erasure), so the following is not permitted:
// â COMPILE ERROR â both methods have the same erasure: process(List)
public void process(List<String> list) { }
public void process(List<Integer> list) { }
3. Practical Usage
3.1 Strategy Pattern
Polymorphism is the foundation of the Strategy pattern â behavior can be swapped at runtime:
public interface SortStrategy {
<T extends Comparable<T>> void sort(List<T> list);
}
public class QuickSort implements SortStrategy {
@Override
public <T extends Comparable<T>> void sort(List<T> list) { /* quicksort */ }
}
public class MergeSort implements SortStrategy {
@Override
public <T extends Comparable<T>> void sort(List<T> list) { /* mergesort */ }
}
// Usage
SortStrategy strategy = useLargeDataset ? new MergeSort() : new QuickSort();
strategy.sort(data);
3.2 Plugin Architectures
Extension points defined via interfaces, with plugins loaded at runtime:
public interface PaymentProcessor {
boolean process(Payment payment);
String getProviderName();
}
// ServiceLoader or Spring IoC auto-discovers implementations
ServiceLoader<PaymentProcessor> processors = ServiceLoader.load(PaymentProcessor.class);
3.3 API Design
Always use the most general type for parameters and return types (program to an interface):
// â
GOOD â polymorphic, accepts any List implementation
public List<String> filterNames(List<String> input) { ... }
// â BAD â tied to a concrete implementation
public ArrayList<String> filterNames(ArrayList<String> input) { ... }
4. Code Examples
4.1 Basic â Overloading + Overriding
public class Shape {
// Overloading (compile-time polymorphism)
public double area(double radius) {
return Math.PI * radius * radius;
}
public double area(double width, double height) {
return width * height;
}
// Method designed for overriding
public String describe() {
return "I am a generic shape";
}
}
public class Circle extends Shape {
private final double radius;
public Circle(double radius) { this.radius = radius; }
@Override
public String describe() {
return "I am a circle with radius " + radius;
}
}
public class Rectangle extends Shape {
private final double w, h;
public Rectangle(double w, double h) { this.w = w; this.h = h; }
@Override
public String describe() {
return "I am a " + w + "Ă" + h + " rectangle";
}
}
4.2 Advanced â Dynamic Dispatch and Covariant Returns
public abstract class Document {
public abstract Document clone();
public abstract String render();
}
public class PdfDocument extends Document {
@Override
public PdfDocument clone() { // covariant return type
return new PdfDocument(/* copy state */);
}
@Override
public String render() {
return "Rendering PDF...";
}
}
public class HtmlDocument extends Document {
@Override
public HtmlDocument clone() { // covariant return type
return new HtmlDocument(/* copy state */);
}
@Override
public String render() {
return "Rendering HTML...";
}
}
// Dynamic dispatch in action
List<Document> docs = List.of(new PdfDocument(), new HtmlDocument());
for (Document doc : docs) {
System.out.println(doc.render()); // runtime type determines the method
Document copy = doc.clone(); // covariant type returned
}
4.3 Advanced â Polymorphism with Interfaces
public interface Drawable {
void draw(Graphics2D g);
}
public interface Resizable {
void resize(double factor);
}
public class CanvasShape implements Drawable, Resizable {
private double scale = 1.0;
@Override
public void draw(Graphics2D g) {
g.scale(scale, scale);
// drawing logic...
}
@Override
public void resize(double factor) {
this.scale *= factor;
}
}
// Either interface reference can be used:
Drawable d = new CanvasShape();
d.draw(graphics);
Resizable r = (Resizable) d;
r.resize(2.0);
5. Trade-offs
| Aspect | Static Polymorphism (overloading) | Dynamic Polymorphism (overriding) |
|---|---|---|
| Resolution time | Compile-time | Runtime |
| Performance | Faster â no runtime lookup | Slight overhead â vtable lookup |
| Flexibility | Limited â bound at compile time | High â swappable at runtime |
| Readability | Many overloads can be confusing | Cleaner with good abstractions |
| Testability | Simple â deterministic | Excellent with mocks |
| Maintenance | Overload proliferation is problematic | Interfaces can remain stable |
| Use case | Convenience methods, builders | Strategy, plugin, framework code |
When to choose which?
- Overloading: Same logic, different input types (e.g.,
valueOf(int),valueOf(String)) - Overriding: Different behavior, shared contract (e.g.,
Collection.size()varies per implementation)
6. Common Mistakes
6.1 â Autoboxing/Widening Confusion in Overloading
public void process(long value) { System.out.println("long"); }
public void process(Integer value) { System.out.println("Integer"); }
process(5); // â "long" â widening (intâlong) takes priority over autoboxing (intâInteger)!
Resolution order: exact match â widening â autoboxing â varargs.
6.2 â Static Method Hiding vs Overriding
public class Parent {
public static void greet() { System.out.println("Parent"); }
}
public class Child extends Parent {
public static void greet() { System.out.println("Child"); } // HIDING, not overriding!
}
Parent ref = new Child();
ref.greet(); // â "Parent" â static methods use COMPILE-TIME type!
Static methods are NOT polymorphic. There is no dynamic dispatch â the declared type of the reference determines which method is called.
6.3 â Missing `@Override` Annotation
public class Dog extends Animal {
// Typo! This is NOT overriding â it's a NEW method
public String speek() { return "Woof!"; }
// â
With @Override the compiler would catch the error:
// @Override
// public String speek() { } // â COMPILE ERROR
}
6.4 â Overloading Instead of Overriding
public class Parent {
public void execute(Object obj) { /* ... */ }
}
public class Child extends Parent {
// â This is overloading, NOT overriding! Different parameter type.
public void execute(String str) { /* ... */ }
}
Parent ref = new Child();
ref.execute("hello"); // â Parent.execute(Object) is called, not the Child version!
7. Senior-level Insights
7.1 vtable and itable in the JVM
The JVM uses virtual method tables (vtable) to implement dynamic dispatch:
| Concept | Description |
|---|---|
| vtable | One table per class â each slot maps to a concrete method implementation pointer. A subclass's vtable extends its superclass's vtable. |
| itable | Interface method table â used for dispatching interface methods. Slower than vtable because the JVM must search through the interface hierarchy. |
| invokevirtual | Bytecode instruction â dispatches via vtable (class methods). |
| invokeinterface | Bytecode instruction â dispatches via itable (interface methods). |
7.2 Monomorphic, Bimorphic, and Megamorphic Call Sites
The JIT compiler optimizes polymorphic call sites based on observed types:
| Call site type | Description | JIT Optimization |
|---|---|---|
| Monomorphic | Always the same 1 type | Inline cache â full inlining |
| Bimorphic | 2 different types observed | Conditional inlining |
| Megamorphic | 3+ different types | No inlining â vtable lookup on every call |
// Monomorphic â optimal
Animal a = new Dog();
a.speak(); // JIT inlines Dog.speak()
// Megamorphic â slower
for (Animal a : mixedAnimals) { // Dog, Cat, Bird, Fish...
a.speak(); // no inlining, vtable lookup every time
}
7.3 `invokedynamic` and Method Handles
The invokedynamic instruction (Java 7+) and the MethodHandle API enable more flexible dispatch:
- Lambda expressions (Java 8+) use
invokedynamicâ they do NOT generate anonymous inner classes. - MethodHandle: A low-level, JVM-optimized method reference that bypasses traditional vtable dispatch.
- Performance:
MethodHandle.invoke()is transparent to the JIT compiler and can be optimized.
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle handle = lookup.findVirtual(Animal.class, "speak",
MethodType.methodType(String.class));
String result = (String) handle.invoke(new Dog()); // â "Woof!"
7.4 Sealed Classes and Pattern Matching (Java 17+)
Sealed classes provide a controlled form of polymorphism â the compiler can verify exhaustiveness:
public sealed interface Shape permits Circle, Rectangle, Triangle {}
// Java 21+ pattern matching switch
String describe(Shape s) {
return switch (s) {
case Circle c -> "Circle r=" + c.radius();
case Rectangle r -> "Rect " + r.w() + "Ă" + r.h();
case Triangle t -> "Triangle";
// no default needed â compiler knows this is exhaustive
};
}
8. Glossary
| Term | Definition |
|---|---|
| Polymorphism | The ability of an object to behave as multiple types |
| Method Overloading | Multiple methods with the same name but different parameter lists within a class |
| Method Overriding | A subclass replaces a superclass method with the same signature |
| Dynamic Dispatch | Runtime method selection based on the actual type of the object |
| Covariant Return Type | An overriding method may return a subtype of the original return type |
| vtable | Virtual method table â the JVM dispatch mechanism for class methods |
| itable | Interface method table â the JVM dispatch mechanism for interface methods |
| Megamorphic | A call site receiving 3+ different types, which the JIT cannot inline |
| invokedynamic | A JVM bytecode instruction for flexible, bootstrap-based method invocation |
| Method Handle | A low-level, JVM-optimized method reference |
| Type Erasure | Removal of generic type information after compilation |
| Static Binding | Method resolution at compile time (overloading, static, private, final methods) |
9. Cheatsheet
- Static polymorphism â method overloading; the compiler resolves the call from the signature.
- Dynamic polymorphism â method overriding; the JVM resolves the call from the runtime type.
- Overload resolution order â exact match â widening â autoboxing â varargs.
- Overriding rules â same signature, same or covariant return type, same or wider access, same or narrower checked exception.
- Never override
static,final, orprivatemethods; always use@Override. - JVM dispatch â
invokevirtualfor class methods,invokeinterfacefor interface methods,invokestaticfor static calls,invokedynamicfor lambda-style bootstrap scenarios. - JIT optimisation â monomorphic call sites optimise best, bimorphic can still inline conditionally, megamorphic sites are harder to optimise.
đź Games
8 questions