IntermediateReading time: ~20 min

Polymorphism

Method overloading, method overriding, dynamic dispatch, covariant return types, vtable/itable and JIT optimization

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. Deep Dive

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. Interview Questions

What is the difference between overloading and overriding?

Overloading is resolved at compile time from the parameter list, while overriding is resolved at runtime from the actual object type.

What does dynamic dispatch mean in Java?

It means the JVM chooses the right overridden implementation at runtime based on the real type of the object behind the reference.

Why can polymorphism hurt performance in some cases?

Highly polymorphic call sites can become harder for the JIT to inline, especially when many different runtime types show up at the same call site.

When does polymorphism become too clever?

When readers cannot understand behavior without jumping through many layers of types, factories, and overrides. Extensibility should not destroy traceability.


9. 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)

10. 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

10 questions