Sitemap
Aishu Reddy

Aishu Reddy

Turning ideas into code and bugs into lessons. 💻✨

Core Java: Polymorphism in Java

5 min readOct 7, 2026

--

The word polymorphism comes from two words:

  • Poly → Many
  • Morphism → Forms or behaviours

So, polymorphism basically means one thing can have many forms or behaviours.

In Java, polymorphism allows the same method name or reference to behave differently depending on the situation.

There are two main types of polymorphism in Java:

  1. Compile-time polymorphism
  2. Runtime polymorphism

1. Compile-Time Polymorphism

Compile-time polymorphism is also called static polymorphism.

Here, Java decides which method needs to be called at compile time.

This is achieved using method overloading.

Method Overloading

Method overloading means having multiple methods with the same name but different parameters in the same class.

For example:

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

Here we have three add() methods.

Their names are the same, but their parameter lists are different.

Calculator calculator = new Calculator();
calculator.add(10, 20);
calculator.add(10, 20, 30);
calculator.add(10.5, 20.5);

Java can determine which method to call based on the arguments we provide.

For example:

calculator.add(10, 20);

Java sees two int arguments, so it chooses:

add(int a, int b)

Whereas:

calculator.add(10, 20, 30);

has three arguments, so Java chooses:

add(int a, int b, int c)

This decision is made during compile time, which is why it is called compile-time polymorphism.

Important point

Method overloading can be done by changing:

  • Number of parameters
  • Type of parameters
  • Order of parameters

For example:

void display(int a, String b)
void display(String a, int b)

But we cannot overload a method only by changing its return type.

This is not valid:

int add(int a, int b)
double add(int a, int b)

because the parameter list is exactly the same.

2. Runtime Polymorphism

Runtime polymorphism is also called dynamic polymorphism.

In runtime polymorphism, Java decides which method to execute at runtime.

Runtime polymorphism is achieved through method overriding.

For example:

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

Here, Dog overrides the sound() method of Animal.

Now:

Dog dog = new Dog();
dog.sound();

Output:

Dog barks

The child class has provided its own implementation of the parent’s method.

Dynamic Method Dispatch

Now we come to one of the most important parts of runtime polymorphism.

We can create a parent-class reference that points to a child-class object.

For example:

Animal animal = new Dog();

Let’s break this down.

Animal animal = new Dog();
↑ ↑
Reference Object

The reference is of type Animal, but the actual object created is a Dog.

This is possible because Dog extends Animal.

Animal
↑
|
Dog

Now if we call:

animal.sound();

Which sound() will execute?

The Dog version:

Dog barks

Why?

Because the actual object is a Dog.

Java determines the overridden method based on the actual object at runtime.

This is called Dynamic Method Dispatch.

Why do we need Dynamic Method Dispatch?

This is where polymorphism becomes really useful.

Get Aishu Reddy’s stories in your inbox

Join Medium for free to get updates from this writer.

Suppose we have different types of animals:

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

Without polymorphism, we might write separate code for every type:

Dog dog = new Dog();
dog.sound();
Cat cat = new Cat();
cat.sound();

But with polymorphism, we can use the parent type:

Animal animal1 = new Dog();
Animal animal2 = new Cat();
animal1.sound();
animal2.sound();

Output:

Dog barks
Cat meows

The same reference type, Animal, is being used, but the behaviour changes depending on the actual object.

This is the real power of polymorphism.

A Real-World Example

Imagine we are building a payment system.

We have a parent class:

class Payment {
    void pay() {
System.out.println("Making payment");
}
}

Then we have different payment methods:

class UPI extends Payment {
    @Override
void pay() {
System.out.println("Payment through UPI");
}
}
class CreditCard extends Payment { @Override
void pay() {
System.out.println("Payment through Credit Card");
}
}
class NetBanking extends Payment { @Override
void pay() {
System.out.println("Payment through Net Banking");
}
}

Now we can write:

Payment payment;

And depending on what payment method we need:

payment = new UPI();
payment.pay();

or:

payment = new CreditCard();
payment.pay();

or:

payment = new NetBanking();
payment.pay();

The method call is the same:

payment.pay();

But the behaviour is different.

UPI        → Payment through UPI
CreditCard → Payment through Credit Card
NetBanking → Payment through Net Banking

This is polymorphism.

Why not simply create the child object directly?

You might wonder:

Why do we write Payment payment = new UPI() instead of simply UPI payment = new UPI()?

For a simple example, both can work.

But the advantage becomes clear when we write reusable code.

For example:

void makePayment(Payment payment) {
payment.pay();
}

Now we can pass any payment type that extends Payment:

makePayment(new UPI());
makePayment(new CreditCard());
makePayment(new NetBanking());

We don’t need to create a separate method for every payment type.

The method only cares that the object is a Payment.

At runtime, Java will execute the correct overridden pay() method.

This gives us:

  • Loose coupling
  • Code reusability
  • Flexibility
  • Easier maintenance
  • The ability to add new child classes without changing existing code

For example, tomorrow we can add:

class Wallet extends Payment {
    @Override
void pay() {
System.out.println("Payment through Wallet");
}
}

And we can simply do:

makePayment(new Wallet());

The makePayment() method does not need to be changed.

One Important Limitation

When we write:

Animal animal = new Dog();

the reference type controls what members are accessible, while the actual object controls which overridden instance method runs.

For example:

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

Now:

Animal animal = new Dog();
animal.sound();  // Allowed
animal.bark(); // Not allowed

Why?

Because the reference is of type Animal.

Java knows that an Animal has sound(), but the Animal reference does not know about the bark() method that exists only in Dog.

However, when sound() is called, Java checks the actual object at runtime and executes Dog's overridden version.

The Easy Way to Remember Polymorphism

Think of it like this:

Compile-time polymorphism

Same method name
↓
Different parameters
↓
Method Overloading
↓
Decision at compile time

Runtime polymorphism

Parent reference
↓
Child object
↓
Method Overriding
↓
Decision at runtime
↓
Dynamic Method Dispatch

So:

Method overloading → Compile-time polymorphism

Method overriding → Runtime polymorphism

Parent reference + child object → Dynamic Method Dispatch

And the biggest benefit of polymorphism is that we can write code against a common parent type while allowing different child objects to provide their own behaviour.

--

--

Aishu Reddy
Aishu Reddy

Turning ideas into code and bugs into lessons. 💻✨