Polymorphism in Java
By the Syncfusion DevTutorials team · Last updated
Use one name for many behaviours with overloading and overriding, @Override, dynamic method dispatch, and safe upcasting and downcasting.
Think about the word "open".
You open a door. You open a book. You open an app on your phone. The word is the same each time, but what actually happens is different. It depends on what you are opening.
Java works the same way. One method name can do different things, depending on the values you pass or the object you call it on.
In simple words: polymorphism means one name, many forms.
The word comes from Greek: poly means "many", and morph means "form".
What You Will Learn
In this lesson, you will learn:
- What polymorphism is, and why it makes code flexible.
- How method overloading gives compile-time polymorphism.
- How method overriding gives runtime polymorphism.
- Why you should always use the
@Overrideannotation. - How Java picks the right method while the program runs.
- How upcasting and downcasting work, and how to downcast safely.
- The differences between compile-time and runtime polymorphism.
Start With a Working Example
Press Run.
One call, three different results
public class Main {
public static void main(String[] args) {
Animal[] zoo = { new Dog(), new Cat(), new Cow() };
for (Animal a : zoo) {
a.speak();
}
}
}
class Animal {
void speak() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
@Override
void speak() {
System.out.println("Dog says Woof");
}
}
class Cat extends Animal {
@Override
void speak() {
System.out.println("Cat says Meow");
}
}
class Cow extends Animal {
@Override
void speak() {
System.out.println("Cow says Moo");
}
}
Expected output:
Dog says Woof
Cat says Meow
Cow says Moo
The loop always calls a.speak(), and a is always an Animal variable. Yet each animal makes its own sound. Java runs the version that belongs to the real object.
Try This
Add a Duck class that extends Animal and overrides speak(). Put a new Duck() in the array. The loop works without any change.
Introduction to Polymorphism
Polymorphism means one name can have many forms.
In Java, it shows up in two ways:
| Kind | Also called | How it works | Decided |
|---|---|---|---|
| Compile-time polymorphism | Static polymorphism | Method overloading | When the code is compiled |
| Runtime polymorphism | Dynamic polymorphism | Method overriding | While the program runs |
Why it matters:
- Flexible code. One loop can handle many types of object.
- Easy to extend. A new class works with old code, as long as it keeps the same method names.
- Less
if-else. You do not need checks like "if it is a dog, bark; if it is a cat, meow". Each object knows its own behaviour.
Compile-Time Polymorphism
Compile-time polymorphism happens when the compiler decides which method to call, before the program runs.
It looks only at the method name and the arguments you pass.
Method Overloading
Method overloading means several methods in the same class have the same name but different parameters.
You learned this in the Methods lesson. Here it is again, as a form of polymorphism.
The compiler picks a version from the arguments
public class Main {
public static void main(String[] args) {
Printer p = new Printer();
p.print(42);
p.print("Hello");
p.print(3.5);
p.print("Total", 250);
}
}
class Printer {
void print(int number) {
System.out.println("Number: " + number);
}
void print(String text) {
System.out.println("Text: " + text);
}
void print(double value) {
System.out.println("Decimal: " + value);
}
void print(String label, int value) {
System.out.println(label + " = " + value);
}
}
Expected output:
Number: 42
Text: Hello
Decimal: 3.5
Total = 250
All four methods are called print(). The compiler looks at each call's arguments and chooses the matching version.
The choice is made when the code is compiled. That is why this is called compile-time polymorphism.
Runtime Polymorphism
Runtime polymorphism happens when Java decides which method to call while the program runs, based on the actual object.
Method Overriding
Method overriding means a child class writes its own version of a method it inherited from its parent.
The overriding method must have:
- the same name as the parent's method;
- the same parameters, in the same order;
- the same return type, or a child type of it;
- the same or wider access. A
publicmethod cannot becomeprivatein the child.
You cannot override static, final or private methods.
Each child gives the inherited method its own body
public class Main {
public static void main(String[] args) {
Payment[] payments = {
new CardPayment(),
new UpiPayment(),
new CashPayment()
};
for (Payment p : payments) {
p.pay(500);
}
}
}
class Payment {
void pay(int amount) {
System.out.println("Paying " + amount);
}
}
class CardPayment extends Payment {
@Override
void pay(int amount) {
System.out.println("Card: charged " + amount + " plus 2% fee = " + (amount + amount * 2 / 100));
}
}
class UpiPayment extends Payment {
@Override
void pay(int amount) {
System.out.println("UPI: sent " + amount + " with no fee");
}
}
class CashPayment extends Payment {
@Override
void pay(int amount) {
System.out.println("Cash: collect " + amount + " at delivery");
}
}
Expected output:
Card: charged 500 plus 2% fee = 510
UPI: sent 500 with no fee
Cash: collect 500 at delivery
The checkout code only calls p.pay(500). It does not need to know which payment method the customer chose.
| Overloading | Overriding | |
|---|---|---|
| Where | In the same class | In a parent and a child class |
| Method name | Same | Same |
| Parameters | Must be different | Must be the same |
| Needs inheritance? | No | Yes |
| Decided | At compile time | At run time |
| Purpose | Same job, different inputs | Same job, different behaviour per type |
@Override Annotation
An annotation is a note for the compiler, written with @.
@Override goes on the line above a method that is meant to override a parent method. It tells the compiler: "check that this really overrides something".
Without it, a small typo creates a new method instead of overriding the old one. The program still compiles, but the parent's version keeps running, and the bug is hard to find.
@Override catches a misspelled method name
public class Main {
public static void main(String[] args) {
Animal a = new Dog();
a.speak();
}
}
class Animal {
void speak() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
@Override
void speek() {
System.out.println("Woof");
}
}
The compiler reports method does not override or implement a method from a supertype. The typo speek was caught straight away.
Without @Override, this program would compile and quietly print Some sound.
Easy Way to Remember: every time you override, write @Override. It costs one line and catches typos in names and parameters.
Dynamic Method Dispatch
Dynamic method dispatch is how Java carries out runtime polymorphism.
Two types are involved whenever you use a variable:
- The reference type is the type of the variable:
Animal a. - The object type is the type of the object it points to:
new Dog().
When you call an overridden method, Java looks at the object type, not the reference type. It finds that object's own version and runs it.
The decision is made while the program runs, because the same variable might point to a different kind of object each time.
The object type decides, not the variable type
public class Main {
public static void main(String[] args) {
Shape s;
s = new Circle();
s.draw();
s = new Square();
s.draw();
s = new Shape();
s.draw();
}
}
class Shape {
void draw() {
System.out.println("Drawing a shape");
}
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("Drawing a circle");
}
}
class Square extends Shape {
@Override
void draw() {
System.out.println("Drawing a square");
}
}
Expected output:
Drawing a circle
Drawing a square
Drawing a shape
s is always a Shape variable. But it points to a Circle, then a Square, then a plain Shape. Each call runs the version that belongs to the object at that moment.
What Is Not Dispatched
Dynamic dispatch only applies to overridden instance methods.
Fields and static methods are chosen by the reference type, at compile time.
Fields follow the variable type, methods follow the object
public class Main {
public static void main(String[] args) {
Parent p = new Child();
System.out.println("Field: " + p.name);
System.out.println("Method: " + p.getName());
}
}
class Parent {
String name = "Parent field";
String getName() {
return "Parent method";
}
}
class Child extends Parent {
String name = "Child field";
@Override
String getName() {
return "Child method";
}
}
Expected output:
Field: Parent field
Method: Child method
The field was chosen by the variable type, Parent. The method was chosen by the object type, Child.
This is one more reason to keep fields private and reach data through methods.
Upcasting and Downcasting
Casting an object means treating it as a different type in the same family.
Upcasting
Upcasting means storing a child object in a parent-type variable.
Animal a = new Dog(); // upcasting
It is always safe, because every Dog is an Animal. Java does it automatically, with no cast written.
After upcasting, you can only call methods the parent type knows about. The Dog object still has its own methods, but the Animal variable cannot see them.
Downcasting
Downcasting means turning a parent-type variable back into a child type, so you can use the child's extra methods.
Dog d = (Dog) a; // downcasting
You must write the cast yourself. It is not always safe. If a actually points to a Cat, the cast fails while the program runs, with a ClassCastException.
Checking With instanceof
Before downcasting, check the real type with instanceof. It returns true if the object is that type (or a child of it).
Modern Java lets you check and cast in one step, with a pattern:
if (a instanceof Dog d) {
d.fetch(); // d is already a Dog here
}
Upcast freely, downcast only after checking
public class Main {
public static void main(String[] args) {
Animal[] pets = { new Dog(), new Cat(), new Dog() };
for (Animal a : pets) {
a.eat();
if (a instanceof Dog d) {
d.fetch();
} else if (a instanceof Cat c) {
c.climb();
}
}
}
}
class Animal {
void eat() {
System.out.println("Eating...");
}
}
class Dog extends Animal {
void fetch() {
System.out.println(" Dog fetches the ball");
}
}
class Cat extends Animal {
void climb() {
System.out.println(" Cat climbs the tree");
}
}
Expected output:
Eating...
Dog fetches the ball
Eating...
Cat climbs the tree
Eating...
Dog fetches the ball
Storing the pets in an Animal[] is upcasting. Every pet can eat().
fetch() only exists on Dog. So the loop checks a instanceof Dog d before using it. For the cat, that check is false, so no unsafe cast happens.
| Upcasting | Downcasting | |
|---|---|---|
| Direction | Child → parent | Parent → child |
| Example | Animal a = new Dog(); | Dog d = (Dog) a; |
| Cast written? | No, automatic | Yes |
| Always safe? | Yes | No, check with instanceof first |
| Methods you can call | Only the parent's | The child's too |
A wrong downcast compiles fine, then stops the program with ClassCastException: class Cat cannot be cast to class Dog. Always check with instanceof before you downcast.
static methods are not overridden. If a child declares a static method with the same signature, it hides the parent's one instead. The version called is chosen by the variable's type, not the object's.
Compile-Time vs Runtime Polymorphism
| Compile-time polymorphism | Runtime polymorphism | |
|---|---|---|
| Achieved by | Method overloading | Method overriding |
| Decided by | The compiler | The JVM, while the program runs |
| Based on | The arguments' types and count | The actual object's type |
| Needs inheritance? | No | Yes |
| Methods involved | Several methods in one class | The same method in parent and child |
| Speed | Slightly faster, no run-time lookup | Tiny run-time lookup |
| Flexibility | Lower | Higher, new types work with old code |
| Example | print(int) and print(String) | Dog.speak() overriding Animal.speak() |
In simple words: overloading picks by what you pass. Overriding picks by what you call it on.
Common mistakes
The following mistakes are common when overriding methods and using polymorphism. Each row shows what you see, why it happens and how to fix it.
| Symptom | Cause | Fix |
|---|---|---|
| The parent's method runs instead of the child's | The child changed the name or parameters | Match the signature, and add @Override |
method does not override or implement a method | @Override found no matching parent method | Fix the name or parameters |
weaker access privileges | The overriding method is less visible | Use the same or wider access |
cannot find symbol on a child method | The variable's type is the parent | Check with instanceof, then use the child type |
ClassCastException | Downcast to a type the object is not | Check with instanceof first |
overridden method is final | The parent method is final | Do not override it, or remove final |
What to Remember
- Polymorphism means one name with many forms.
- Overloading gives compile-time polymorphism: same name, different parameters, chosen by the compiler.
- Overriding gives runtime polymorphism: a child replaces a parent method, and the real object decides which version runs.
- Always write
@Override. It catches typos in names and parameters. - Dynamic method dispatch picks methods by the object's type. Fields and static methods follow the variable's type.
- Upcasting is automatic and safe. Downcasting needs a cast, so check with
instanceoffirst.
Next: Abstraction