Skip to content
Java polymorphism 6 min read

Runtime Polymorphism

Runtime polymorphism is one of the most powerful features in object-oriented Java. It lets you write code that works against a parent type, yet automatically calls the right method for whatever subclass object actually shows up — all decided at runtime, not at compile time.

What Is Runtime Polymorphism?

Runtime polymorphism (also called dynamic method dispatch or late binding) happens when an overridden method is resolved at runtime through a parent-type reference. The JVM looks at the actual object sitting in memory, not the declared type of the variable, to decide which method to call.

This is different from compile-time polymorphism (method overloading), where the compiler picks the method before the program ever runs.

Note: Runtime polymorphism requires method overriding — a subclass must redefine a method it inherited from its parent.

A Simple Example

class Animal {
    void sound() {
        System.out.println("Some generic animal sound");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Woof!");
    }
}

class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("Meow!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a;          // declared type: Animal

        a = new Dog();     // actual type: Dog
        a.sound();

        a = new Cat();     // actual type: Cat
        a.sound();
    }
}

Output:

Woof!
Meow!

The variable a is declared as Animal, but the JVM calls Dog.sound() and Cat.sound() respectively — because that is the actual object at runtime.

Upcasting: The Trigger

Runtime polymorphism is enabled by upcasting — assigning a subclass object to a parent-type reference. Java does this implicitly (no cast keyword needed) because every subclass is-a parent.

Animal a = new Dog();   // implicit upcast: Dog → Animal

You can also pass upcasted references into methods:

class Main {
    static void makeSound(Animal a) {  // accepts any Animal subtype
        a.sound();
    }

    public static void main(String[] args) {
        makeSound(new Dog());
        makeSound(new Cat());
    }
}

Output:

Woof!
Meow!

This is the real-world power of runtime polymorphism: your makeSound method never needs to change, even when you add a new subclass months later.

Tip: This pattern is the foundation of the Open/Closed Principle — open for extension, closed for modification.

Rules for Runtime Polymorphism

Runtime polymorphism has a few conditions:

RequirementDetails
InheritanceThere must be an IS-A relationship (extends or implements)
Method overridingThe subclass must override the parent method
Parent-type referenceThe variable must be declared as the parent (or interface) type
Object is a subtypeThe actual object created must be a subclass instance

Warning: Runtime polymorphism applies only to instance methods. It does NOT work for static methods, instance variables, or static variables — those are resolved at compile time based on the reference type.

Static Methods Are NOT Polymorphic

class Parent {
    static void greet() {
        System.out.println("Hello from Parent");
    }
}

class Child extends Parent {
    static void greet() {
        System.out.println("Hello from Child");
    }
}

public class Main {
    public static void main(String[] args) {
        Parent p = new Child();
        p.greet();   // calls Parent.greet() — NOT Child.greet()
    }
}

Output:

Hello from Parent

Static methods belong to the class, not the object. The compiler resolves them at compile time based on the reference type. This is called method hiding, not overriding.

Instance Variables Are NOT Polymorphic

class Parent {
    int value = 10;
}

class Child extends Parent {
    int value = 20;
}

public class Main {
    public static void main(String[] args) {
        Parent p = new Child();
        System.out.println(p.value);  // prints 10, NOT 20
    }
}

Output:

10

Field access is compile-time bound. Only methods participate in runtime dispatch.

Polymorphism With Interfaces

Interfaces are another powerful way to achieve runtime polymorphism. An interface reference can point to any class that implements it.

interface Shape {
    double area();
}

class Circle implements Shape {
    double radius;
    Circle(double r) { this.radius = r; }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

class Rectangle implements Shape {
    double width, height;
    Rectangle(double w, double h) { this.width = w; this.height = h; }

    @Override
    public double area() {
        return width * height;
    }
}

public class Main {
    public static void main(String[] args) {
        Shape[] shapes = { new Circle(5), new Rectangle(4, 6) };

        for (Shape s : shapes) {
            System.out.printf("Area: %.2f%n", s.area());
        }
    }
}

Output:

Area: 78.54
Area: 24.00

See Interfaces for more details on using interface references.

Multi-Level Inheritance and Dispatch

When inheritance chains are deeper, the JVM always calls the most specific (most derived) override it can find.

class A {
    void hello() { System.out.println("A"); }
}

class B extends A {
    @Override
    void hello() { System.out.println("B"); }
}

class C extends B {
    // does NOT override hello()
}

public class Main {
    public static void main(String[] args) {
        A obj = new C();
        obj.hello();   // walks up to B (closest override)
    }
}

Output:

B

The JVM walks up the class hierarchy from C → finds B.hello() → calls it.

Under the Hood: vtable & Dynamic Dispatch

When the JVM loads a class, it builds a virtual method table (vtable) — an array of method pointers for every overridable (virtual) method. Subclass vtables copy their parent’s entries and replace any that the subclass overrides.

At a call site like a.sound(), the JVM:

  1. Reads the actual type of the object referenced by a.
  2. Looks up the vtable slot for sound() on that type.
  3. Jumps to the method pointer stored there.

This lookup costs one extra memory indirection compared to a static call, but the JIT compiler can eliminate it through a technique called inline caching and devirtualization — if the JIT observes that a call site always sees the same concrete type, it compiles it as a direct call. So in hot paths the overhead is effectively zero.

All non-private, non-final, non-static instance methods in Java are virtual by default. You can read the full details in vtable & Dynamic Dispatch and JIT Compilation & Bytecode.

Note: Marking a method or class final prevents overriding and lets the JIT skip the vtable lookup entirely — a small but real performance benefit in tight loops.

Compile-Time vs. Runtime Polymorphism

FeatureCompile-TimeRuntime
MechanismMethod overloadingMethod overriding
Resolved atCompile timeRuntime
Based onParameter typesActual object type
Also calledStatic bindingDynamic binding
Works withStatic & instance methodsInstance methods only
PerformanceSlightly fasterJIT can optimize to equal

See Overloading vs Overriding for a deeper comparison.

Last updated June 13, 2026
Was this helpful?