Polymorphism

Lesson 0009 — 35 min read

Polymorphism — "many forms." The same method or object behaves differently depending on context. Two types: compile-time (static) and run-time (dynamic).

1. Compile-Time Polymorphism (Static)

The method to call is resolved at compile time based on signature (name + parameters). Achieved via method overloading. Also called early binding.

Note: return type alone cannot differentiate overloaded methods.

class Calculator {
    int add(int a, int b) { return a + b; }
    double add(double a, double b) { return a + b; }
}

Calculator calc = new Calculator();
System.out.println(calc.add(5, 3));     // int version
System.out.println(calc.add(5.5, 3.3)); // double version
class Calculator {
public:
    int add(int a, int b) { return a + b; }
    double add(double a, double b) { return a + b; }
};

Calculator calc;
cout << calc.add(5, 3) << endl;       // int version
cout << calc.add(5.5, 3.3) << endl;   // double version
class Calculator:
    def add(self, a, b):
        return a + b  # works for both — Python is dynamically typed

calc = Calculator()
print(calc.add(5, 3))       # ints
print(calc.add(5.5, 3.3))   # floats

Keypoint: Compiler chooses the right method at compile time by matching argument types. Faster (early binding). No runtime overhead.

2. Run-Time Polymorphism (Dynamic)

The method to call is resolved at runtime based on the actual object type, not the reference type. Achieved via method overriding. Also called late binding or dynamic method dispatch.

class Animal {
    void sound() { System.out.println("Animal makes a sound"); }
}

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

Animal ref = new Dog();   // Animal reference, Dog object
ref.sound();              // "Dog barks" — resolved at runtime
class Animal {
public:
    virtual void sound() { cout << "Animal makes a sound" << endl; }
};

class Dog : public Animal {
public:
    void sound() override { cout << "Dog barks" << endl; }
};

Animal* ptr = new Dog();   // Animal pointer, Dog object
ptr->sound();              // "Dog barks" — vtable lookup at runtime
delete ptr;
class Animal:
    def sound(self):
        print("Animal makes a sound")

class Dog(Animal):
    def sound(self):
        print("Dog barks")

myAnimal = Dog()
myAnimal.sound()    # "Dog barks" at runtime

Keypoint: JVM/compiler generates code that checks the actual object type at runtime and dispatches to the correct overridden method. Slightly slower (vtable/dispatch table lookup).

Comparison: Compile-Time vs Run-Time

Aspect Compile-Time Run-Time
When resolved At compile time At runtime
Mechanism Method overloading Method overriding
Binding Early (static) Late (dynamic)
Speed Faster Slightly slower (dispatch overhead)
Inheritance needed? No Yes

Why Runtime Polymorphism Matters

It lets you write code that works on a parent reference but calls the child's implementation. This is the foundation of:

Code trace: What does this print?

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

class B extends A {
    void show() { System.out.println("B"); }
}

class C extends A {
    void show() { System.out.println("C"); }
}

A[] arr = {new A(), new B(), new C()};
for (A x : arr) x.show();

Can two overloaded methods differ only by return type?

Notes

Polymorphism :-

Polymorphism ⇒ "many forms" ; same method or object reference behaves differently depending on context

Compile-Time Polymorphism (Static) :-

Resolution ⇒ resolved at compile time based on method signature

Mechanism ⇒ method overloading (same class, different parameters)

Early binding ⇒ faster execution, no runtime lookup overhead

Restriction ⇒ return type alone cannot differentiate overloaded methods

Run-Time Polymorphism (Dynamic) :-

Resolution ⇒ resolved at runtime based on actual object type

Mechanism ⇒ method overriding (parent and child classes)

Late binding ⇒ vtable / dynamic dispatch lookup

Behavior ⇒ parent reference holding child object invokes child's overridden method

Key Differences :-

Overloading ⇒ compile-time ; same class ; different parameters

Overriding ⇒ runtime ; parent-child ; exact same parameters

Python ⇒ duck typing ; no compile-time method overloading

C++ ⇒ requires virtual keyword for dynamic dispatch

Java ⇒ all non-static methods are virtual by default

Real-World Uses :-

a) Strategy pattern ⇒ swap algorithms at runtime via interface reference

b) Dependency injection ⇒ accept any implementation of a contract

c) Collections ⇒ List<String> list = new ArrayList<>()

Primary source: Oracle Java Tutorials — Polymorphism. Ask me anything that's unclear.

← Prev: Inheritance Next: Abstraction →

Questions? Ask your agent — you can follow up on any concept, quiz answer, or how this applies to a specific coding problem. Review the Java glossary or syntax quick reference for a compressed overview.