Core Java: Polymorphism in Java
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:
- Compile-time polymorphism
- 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 barksThe 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 ObjectThe reference is of type Animal, but the actual object created is a Dog.
This is possible because Dog extends Animal.
Animal
↑
|
DogNow if we call:
animal.sound();Which sound() will execute?
The Dog version:
Dog barksWhy?
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 meowsThe 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 BankingThis is polymorphism.
Why not simply create the child object directly?
You might wonder:
Why do we write
Payment payment = new UPI()instead of simplyUPI 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 allowedWhy?
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 timeRuntime polymorphism
Parent reference
↓
Child object
↓
Method Overriding
↓
Decision at runtime
↓
Dynamic Method DispatchSo:
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.




