Java Polymorphism

Polymorphism means "many forms." In Java, polymorphism allows a single method, variable, or object to take on different forms depending on the context. It is one of the most powerful features of OOP — it makes programs more flexible, extensible, and easier to maintain.

There are two types of polymorphism in Java: Compile-time Polymorphism (also called static polymorphism) and Runtime Polymorphism (also called dynamic polymorphism).

1. Compile-Time Polymorphism – Method Overloading

Compile-time polymorphism is achieved through method overloading. Multiple methods share the same name but differ in their parameter list (number, type, or order of parameters). Java resolves which method to call at compile time based on the method signature.

Example – Overloaded calculate() Method

class Calculator {

    int calculate(int a, int b) {
        return a + b;
    }

    double calculate(double a, double b) {
        return a + b;
    }

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

public class Main {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        System.out.println(calc.calculate(5, 3));           // 8
        System.out.println(calc.calculate(2.5, 3.5));       // 6.0
        System.out.println(calc.calculate(10, 20, 30));     // 60
    }
}

The correct calculate() method is chosen at compile time based on the arguments provided.

2. Runtime Polymorphism – Method Overriding

Runtime polymorphism is achieved through method overriding combined with inheritance. A parent class reference variable can hold a child class object. When an overridden method is called, Java determines which version to run at runtime based on the actual object type — not the reference type. This mechanism is called Dynamic Method Dispatch.

Example – Notification System

class Notification {
    void send() {
        System.out.println("Sending a notification...");
    }
}

class EmailNotification extends Notification {
    @Override
    void send() {
        System.out.println("Sending Email notification.");
    }
}

class SMSNotification extends Notification {
    @Override
    void send() {
        System.out.println("Sending SMS notification.");
    }
}

class PushNotification extends Notification {
    @Override
    void send() {
        System.out.println("Sending Push notification.");
    }
}

public class Main {
    public static void main(String[] args) {
        Notification n;

        n = new EmailNotification();
        n.send();   // Sending Email notification.

        n = new SMSNotification();
        n.send();   // Sending SMS notification.

        n = new PushNotification();
        n.send();   // Sending Push notification.
    }
}

Output:

Sending Email notification.
Sending SMS notification.
Sending Push notification.

The same reference variable n calls different versions of send() depending on the actual object assigned to it at runtime.

Polymorphism with Arrays

One of the most practical uses of runtime polymorphism is processing a collection of different subtypes through a common parent reference.

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

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

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

class Cow extends Animal {
    @Override
    void sound() { System.out.println("Cow: Moo!"); }
}

public class Main {
    public static void main(String[] args) {
        Animal[] animals = { new Dog(), new Cat(), new Cow() };

        for (Animal a : animals) {
            a.sound();   // correct version called for each object
        }
    }
}

Output:

Dog: Woof!
Cat: Meow!
Cow: Moo!

Upcasting and Downcasting

Upcasting (Implicit)

Assigning a child class object to a parent class reference is called upcasting. This happens automatically (implicit) and is always safe.

Animal a = new Dog();   // upcasting – Dog object stored in Animal reference

Downcasting (Explicit)

Converting a parent class reference back to a child class reference is called downcasting. This must be done explicitly and should be verified with instanceof to avoid a ClassCastException.

Animal a = new Dog();   // upcasting

if (a instanceof Dog) {
    Dog d = (Dog) a;   // downcasting
    d.bark();
}

The instanceof Operator

The instanceof operator checks whether an object is an instance of a particular class or subclass, returning true or false.

Animal a = new Dog();
System.out.println(a instanceof Animal);   // true
System.out.println(a instanceof Dog);      // true
System.out.println(a instanceof Cat);      // false

Overloading vs Overriding

FeatureMethod OverloadingMethod Overriding
TypeCompile-time polymorphismRuntime polymorphism
ClassSame classParent and child class
Method nameSameSame
ParametersDifferentSame
Return typeCan differMust be same (or covariant)
Inheritance needed?NoYes

Benefits of Polymorphism

  • Flexibility: New subclasses can be added without changing existing code.
  • Extensibility: Systems grow naturally without rewriting core logic.
  • Simplicity: One interface works for many types of objects.

Summary

  • Polymorphism allows objects or methods to take many forms.
  • Compile-time polymorphism is achieved via method overloading — resolved at compile time.
  • Runtime polymorphism is achieved via method overriding — resolved at runtime based on the actual object type.
  • A parent class reference can hold a child class object (upcasting).
  • Use instanceof before downcasting to prevent ClassCastException.
  • Polymorphism makes programs extensible — new types can be added with minimal changes to existing code.

Leave a Comment

Your email address will not be published. Required fields are marked *