Skip to content
Part IA Michaelmas Term

Static versus Dynamic Polymorphism

Two Kinds of Polymorphism

Java provides two distinct forms of polymorphism, operating at different times and by different mechanisms. Confusing them is a common source of bugs and a frequent Tripos pitfall.

Static Polymorphism: Method Overloading

Overloading is resolved at compile time by the compiler, using the declared (static) types of the arguments. Multiple methods in the same class share a name but have different parameter lists (different number, types, or order of parameters).

class Printer {
    void print(int x)       { System.out.println("int: " + x); }
    void print(double x)    { System.out.println("double: " + x); }
    void print(String x)    { System.out.println("String: " + x); }
    void print(int x, int y){ System.out.println("two ints: " + x + ", " + y); }
}

Printer p = new Printer();
p.print(42);           // "int: 42" — compiler picks print(int)
p.print(3.14);         // "double: 3.14" — compiler picks print(double)
p.print("hello");      // "String: hello"
p.print(1, 2);         // "two ints: 1, 2"

The compiler decides which overload to call based purely on the argument types at the call site. This decision is baked into the bytecode — the runtime type of the arguments is irrelevant.

Overloading Resolution with Subtypes

The compiler picks the most specific applicable method:

class Animal { }
class Dog extends Animal { }

void handle(Animal a) { System.out.println("Animal handler"); }
void handle(Dog d)    { System.out.println("Dog handler"); }

Animal a = new Dog();
Dog d = new Dog();

handle(a);  // "Animal handler" — declared type of a is Animal
handle(d);  // "Dog handler" — declared type of d is Dog

Even though a holds a Dog object at runtime, the overloaded method is chosen based on a’s declared type (Animal). The runtime type does not enter into overloading resolution.

Ambiguous Overloads

If the compiler cannot determine a single most specific overload, it reports a compile error:

void m(int x, double y) { }
void m(double x, int y) { }

m(1, 2);  // compile error: ambiguous — both are equally applicable

Dynamic Polymorphism: Method Overriding

Overriding is resolved at runtime by the JVM using the object’s runtime type. A subclass provides a new implementation for an inherited method with the same signature.

class Animal {
    String sound() { return "..."; }
}

class Dog extends Animal {
    @Override
    String sound() { return "Woof"; }
}

class Cat extends Animal {
    @Override
    String sound() { return "Meow"; }
}

Animal a1 = new Dog();
Animal a2 = new Cat();

System.out.println(a1.sound());  // "Woof" — runtime type Dog
System.out.println(a2.sound());  // "Meow" — runtime type Cat

Both a1 and a2 have declared type Animal. But the JVM dispatches sound() based on the actual object’s class.

The Virtual Method Table (Vtable)

Dynamic dispatch is implemented using a virtual method table (vtable), also known as a dispatch table.

Vtable mechanism for runtime method resolution

Each class has a vtable — an array of function pointers, one entry per virtual method. When a method is overridden in a subclass, the relevant entry in the subclass’s vtable points to the new implementation. When the JVM executes obj.method(), it:

  1. Follows the object reference to the heap object.
  2. Reads the object’s hidden class pointer (a reference to the Class metadata).
  3. Indexes into that class’s vtable at the known offset for method.
  4. Jumps to the function pointer stored there.

This adds one level of indirection per virtual method call. It is a small cost, and modern JVMs aggressively optimise it away with inline caching and devirtualisation.

Key Differences: Summary Table

Overloading (Static)Overriding (Dynamic)
ResolvedCompile timeRuntime
MechanismCompiler selects the most specific method based on argument typesJVM looks up the vtable based on the object’s runtime class
Depends onDeclared types of argumentsRuntime type of the receiver object
WhereSame class or subclass (methods with same name, different params)Class hierarchy (methods with same signature)
Return typeCan be differentMust be same or covariant
static methodsCan be overloadedCannot be overridden (hidden instead)
final methodsCan be overloadedCannot be overridden

Both Mechanisms in One Call

A single method call can involve both overloading resolution and overriding dispatch:

class Parent {
    void m(Animal a) { System.out.println("Parent.m(Animal)"); }
}

class Child extends Parent {
    @Override
    void m(Animal a) { System.out.println("Child.m(Animal)"); }
    void m(Dog d)    { System.out.println("Child.m(Dog)"); }  // overload, not override
}

Parent p = new Child();
Animal a = new Dog();
Dog d = new Dog();

p.m(a);  // "Child.m(Animal)"
         // Overloading: declared type of a is Animal → m(Animal)
         // Overriding: runtime type of p is Child → Child.m(Animal)
p.m(d);  // "Child.m(Animal)"
         // Overloading: declared type of p is Parent → only m(Animal) visible
         // Overriding: runtime type of p is Child → Child.m(Animal)

Even though d is a Dog and Child has an m(Dog) overload, p’s declared type is Parent, which only knows about m(Animal). Overloading resolution uses the declared type of the reference the method is called on; overriding dispatch uses the runtime type of the object.