Skip to content

Polymorphism — Compile-Time Overloading, Runtime Overriding, and Covariant Types

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Polymorphism. We cover key concepts, practical examples, and best practices to help you master this topic.

Polymorphism in Java allows objects to take many forms, with compile-time polymorphism via method overloading and runtime polymorphism via method overriding. The word "polymorphism" comes from Greek — "poly" (many) and "morph" (form) — meaning a single interface can represent multiple underlying implementations.

What You'll Learn

  • Compile-time polymorphism (method overloading)
  • Runtime polymorphism (dynamic method dispatch)
  • Covariant return types
  • Polymorphism in collections and frameworks

Why It Matters

Polymorphism is the foundation of the Strategy pattern, the Template Method pattern, and virtually every Java framework. When you call list.sort(comparator), the sort method is polymorphic — it works with any List implementation and any Comparator.

Real-World Use

Spring's @Transactional works with any class. Hibernate's Session.save() works with any entity. Java's Collections.sort() works with any List. All of these rely on polymorphic method dispatch.


Compile-Time Polymorphism (Method Overloading)

The compiler decides which method to call based on argument types and count:

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

    public int add(int a, int b, int c) {
        return a + b + c;
    }

    public double add(double a, double b) {
        return a + b;
    }
}

Calculator calc = new Calculator();
calc.add(2, 3);       // calls add(int, int)
calc.add(2, 3, 4);    // calls add(int, int, int)
calc.add(2.5, 3.5);   // calls add(double, double)

The binding happens at compile time because the compiler knows the exact types of the arguments.

Automatic Resolution Order

When the compiler resolves overloaded methods, it tries:

  1. Exact match
  2. Widening primitive conversion (int -> long -> double)
  3. Autoboxing (int -> Integer)
  4. Varargs
public void show(int i) { System.out.println("int"); }
public void show(long l) { System.out.println("long"); }
public void show(Integer i) { System.out.println("Integer"); }

show(5);       // "int" (exact match wins)
show(Integer.valueOf(5)); // "Integer" (exact match)

Runtime Polymorphism (Method Overriding)

The JVM decides which method to call at runtime based on the actual object type:

class Animal {
    void speak() {
        System.out.println("Animal speaks");
    }
}

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

class Cat extends Animal {
    @Override
    void speak() {
        System.out.println("Cat meows");
    }
}
Animal[] animals = {new Dog(), new Cat(), new Animal()};
for (Animal a : animals) {
    a.speak(); // runtime dispatch
}

Output:

Dog barks
Cat meows
Animal speaks

Dynamic Method Dispatch

The JVM looks at the actual object type (not the reference type) to decide which method to call. This is called virtual method invocation and happens via the vtable (virtual method table) stored in the class's method area.

Polymorphism with Interfaces

interface PaymentProcessor {
    void process(double amount);
}

class CreditCardProcessor implements PaymentProcessor {
    @Override
    public void process(double amount) {
        System.out.println("Processing credit card: $" + amount);
    }
}

class PayPalProcessor implements PaymentProcessor {
    @Override
    public void process(double amount) {
        System.out.println("Processing PayPal: $" + amount);
    }
}

PaymentProcessor processor = getProcessor(); // returns CreditCardProcessor or PayPalProcessor
processor.process(100.0); // runtime decides which implementation

Covariant Return Types

An overriding method can return a more specific type than the parent:

class Animal {
    Animal reproduce() {
        return new Animal();
    }
}

class Dog extends Animal {
    @Override
    Dog reproduce() {  // covariant return type
        return new Dog();
    }
}

This was added in Java 5. Before that, overrides required an identical return type.

Polymorphism in Collections

List<String> list = new ArrayList<>();     // List is interface, ArrayList is implementation
list = new LinkedList<>();                 // swap implementations easily
list = Collections.synchronizedList(list); // add behavior via decoration

This is the power of programming to interfaces rather than implementations.

Polymorphism and instanceof

Sometimes you need to check the actual type at runtime:

public void handleAnimal(Animal animal) {
    if (animal instanceof Dog dog) {
        dog.fetch(); // Dog-specific method
    } else if (animal instanceof Cat cat) {
        cat.scratch(); // Cat-specific method
    } else {
        animal.speak();
    }
}

Pattern matching for instanceof (Java 16+) simplifies this.

Common Mistakes

  1. Overriding a static method. Static methods are hidden, not overridden. The method called depends on the reference type, not the object type.
  2. Calling overridable methods from constructors. The subclass's override runs before the subclass constructor body, potentially using uninitialized fields.
  3. Confusing overloading with overriding. Overloading = same name, different parameters. Overriding = same signature, different class.
  4. Using instanceof excessively. Frequent instanceof checks often indicate a missed polymorphism opportunity — consider adding a method to the parent class.
  5. Forgetting that private methods are not polymorphic. Private methods are implicitly final and cannot be overridden.

Practice Questions

1. What is the difference between compile-time and runtime polymorphism?
Compile-time (overloading): method selected by the compiler based on argument types. Runtime (overriding): method selected by the JVM based on actual object type.

2. Can you override a private method?
No. Private methods are not visible to subclasses and are implicitly final. A subclass can declare a method with the same name, but it is a new method, not an override.

3. What is a covariant return type?
An overriding method that returns a subtype of the parent's return type. Example: parent returns Animal, child returns Dog.

4. How does the JVM implement runtime polymorphism?
Through a virtual method table (vtable). Each class has a vtable with method pointers. When a virtual method is called, the JVM looks up the method in the object's class vtable.

5. What is the relationship between polymorphism and the Strategy pattern?
The Strategy pattern uses polymorphism to define a family of algorithms. The context holds a reference to a strategy interface, and the actual strategy is determined at runtime.

Challenge Question:
Design a polymorphic payment processing system. Create an Payment interface with void pay(double amount), implement CreditCard, PayPal, and Crypto classes. Write a checkout(Payment payment, double amount) method that works with any payment type. Add a Refundable interface for payments that support refunds and use instanceof to check.

FAQ

What is the difference between polymorphism and inheritance?

Inheritance is the mechanism that enables code reuse and establishes an is-a relationship. Polymorphism is the ability to use a derived class through a base class reference and get the correct behavior. Inheritance provides the structure; polymorphism provides the behavior.

Can you have polymorphism without inheritance?

Yes, through interfaces. A class can implement multiple interfaces, and you can write code against an interface reference. This is interface polymorphism and is preferred over class inheritance in many cases.

What is the performance cost of polymorphism?

There is a small overhead from the vtable lookup (one extra pointer dereference compared to a direct call). Modern JVMs optimize this with inline caching and JIT compilation. The benefit of maintainable code far outweighs the minimal performance cost.

Can you overload or override a constructor?

Constructors cannot be overridden (they are not inherited). They can be overloaded within the same class.

What is the difference between `overloading` and `hiding`?

Overriding replaces a parent method with a child method. Hiding occurs when a subclass declares a static method with the same signature as a parent static method — both methods exist independently.

Mini Project

Write a program PolymorphismDemo.java that:

  1. Defines an abstract Shape class with an abstract method double area()
  2. Creates Circle, Rectangle, and Triangle subclasses
  3. Demonstrates compile-time polymorphism by overloading a printArea method (accepting Shape, Collection<Shape>, and varargs)
  4. Demonstrates runtime polymorphism by storing different shapes in a Shape[] and calling area() in a loop
  5. Uses covariant return types: have Shape return Shape duplicate() and Circle return Circle duplicate()
  6. Shows the limitation: try overriding a final method and observe the compile error

What's Next

Polymorphism lets you write flexible code, but sometimes you need to define partial implementations that subclasses must complete. Lesson 15 introduces abstract classes and the template method pattern, which let you define a skeleton algorithm while leaving specific steps to subclasses.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro