Intermediate Reading time: ~11 min

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, or private methods; always use @Override.
  • JVM dispatch — invokevirtual for class methods, invokeinterface for interface methods, invokestatic for static calls, invokedynamic for 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