Java Polymorphism
Table of Contents + −
In the last lesson you learned about Java method overriding. A Dog and a Cat both overrode makeSound, each in its own way. That sets up polymorphism. You treat many different objects the same way, and still get each one’s own behavior.
🤔 What problem does polymorphism solve?
Say you manage a zoo app with a list of animals. Each must make its own sound at feeding time. Without polymorphism:
- You check the type of every animal by hand.
- Then you call the matching method for that type.
- Every new animal means another check to add.
That clunky code is the trap polymorphism saves you from.
// ❌ the clunky way: check each type by handif (animal is a Dog) dog.bark();else if (animal is a Cat) cat.meow();else if (animal is a Cow) cow.moo();It grows forever and it is fragile. Forget one branch and an animal stays silent. What you want instead:
- Just say
animal.makeSound()and let each animal do its own thing. - No type checking.
- No long
ifchain.
One call, and the correct behavior happens for whatever the object actually is.
🧩 What is polymorphism?
“Poly” means many. “Morph” means form. So polymorphism means many forms: one method call takes many forms of behavior, depending on the actual object behind it.
Think of a “Play” button. On a music app it plays a song. The same button on a video app plays a video. Same name, different result, based on what is behind it.
Java has two kinds:
- Compile-time polymorphism: from method overloading. Java decides which method to run while it compiles.
- Runtime polymorphism: from method overriding via inheritance or interfaces. Java decides while the program runs, based on the real object.
Runtime is the powerful kind, so most of this lesson is about it.
🔑 The trick that makes runtime polymorphism work
The heart of it: a parent-type variable can hold a child object.
Animal a = new Dog(); // a Dog is-a Animal, so this is fineHere is what happens when you call a.makeSound():
ais declaredAnimal, but the real object is a Dog.- Java ignores the declared type when choosing the method.
- Java looks at the real object and runs the Dog’s
makeSound. - This choosing-at-runtime is dynamic dispatch, the engine of runtime polymorphism.
The rule that trips people up:
- The declared variable type decides what methods you are allowed to call.
- The actual object decides which version actually runs.
💡 Polymorphism in action
An Animal parent with two children that override makeSound. We treat them all as plain animals.
class Animal { void makeSound() { System.out.println("Some sound"); }}
class Dog extends Animal { @Override void makeSound() { // ✅ Dog's own version System.out.println("Woof"); }}
class Cat extends Animal { @Override void makeSound() { // ✅ Cat's own version System.out.println("Meow"); }}
public class Main { public static void main(String[] args) { Animal a1 = new Dog(); // parent reference, Dog object Animal a2 = new Cat(); // parent reference, Cat object
a1.makeSound(); // runs Dog's version a2.makeSound(); // runs Cat's version }}Walk through it:
- Both
a1anda2are declaredAnimal. a1really holds a Dog,a2really holds a Cat.- Java picks
makeSoundby the actual object, soa1says “Woof” anda2says “Meow”. - Type is
Animalboth times, but the behavior belongs to the child. That is runtime polymorphism.
Output
WoofMeowNotice @Override above each child method. Not required, but always write it:
- It tells Java “I mean to override the parent method here”.
- Spell the name or parameters wrong and Java stops and warns you.
- Without it, Java silently makes a brand new method instead.
🔁 The real payoff: one loop for all
Put different objects in one array and treat them all the same. No type checks, no if chains, one clean loop.
public class Main { public static void main(String[] args) { Animal[] animals = { new Dog(), new Cat(), new Animal() };
for (Animal a : animals) { a.makeSound(); // ✅ each object does its own thing } }}What this loop does:
- Stores a Dog, a Cat, and a plain Animal in one
Animal[]. - Calls
makeSoundon each, and every object answers with its own version. - We never ask “is this a Dog?”.
- Add a
Cowclass tomorrow and the loop handles it with zero changes. That flexibility is why polymorphism is loved.
Output
WoofMeowSome soundThis is called dynamic method dispatch
The full technical name for “Java picks the child’s overridden method at runtime based on the real object” is dynamic method dispatch, sometimes called dynamic binding. You do not need to memorize the term to use polymorphism. But you may meet it in interviews. It is just runtime polymorphism in action.
📐 A bigger worked example: shapes with area
A drawing tool where every shape reports its area. A circle computes one way, a rectangle another. Write the area logic once per shape, then treat them all as plain Shape objects and add up the areas.
class Shape { double area() { // generic fallback return 0; }}
class Circle extends Shape { double radius; Circle(double radius) { this.radius = radius; } @Override double area() { // ✅ circle's own formula return Math.PI * radius * radius; }}
class Rectangle extends Shape { double width; double height; Rectangle(double width, double height) { this.width = width; this.height = height; } @Override double area() { // ✅ rectangle's own formula return width * height; }}
public class Main { public static void main(String[] args) { Shape[] shapes = { new Circle(2), new Rectangle(3, 4), new Circle(1) };
double total = 0; for (Shape s : shapes) { System.out.println("Area: " + s.area()); total += s.area(); // each s knows its own area } System.out.println("Total area: " + total); }}Look at the loop:
sis just aShape; we never check if it is a circle or rectangle.- We call
s.area()and the right formula runs for each real object. - Circle uses pi times radius squared. Rectangle uses width times height.
- The loop stays the same no matter how many shape types you add. Add a Triangle later and this code does not change.
Output
Area: 12.566370614359172Area: 12.0Area: 3.141592653589793Total area: 27.707963227948964Write code against the parent type, let each child fill in the details. That keeps the calling code short and stable.
🔼 Upcasting and downcasting
Putting a child object into a parent-type variable is upcasting:
- Always works, because a Dog truly is-a Animal.
- Safe and automatic, no cast needed by hand.
Dog d = new Dog();Animal a = d; // ✅ upcasting, always safe and automatica.makeSound(); // still runs Dog's versionDowncasting is the opposite: take a parent-type reference and treat it as the child type again.
- You need it to call a method only the child has, like a Dog’s
fetch. - Not automatic; you write the cast yourself.
- You must be sure the object really is that child.
Animal a = new Dog();Dog d = (Dog) a; // downcasting back to Dogd.fetch(); // ✅ now we can call Dog-only methodsIf the object is not actually a Dog, the downcast crashes at runtime with a ClassCastException. So check first with instanceof, which asks “is this object actually of that type?” and returns true or false. The safe pattern:
Animal a = new Cat();
if (a instanceof Dog) { Dog d = (Dog) a; // safe, only runs when a really is a Dog d.fetch();} else { System.out.println("Not a dog, skipping fetch");}Modern Java combines the check and the cast in one line, called pattern matching for instanceof.
Animal a = new Dog();
if (a instanceof Dog d) { // checks and casts in one step d.fetch(); // d is ready to use here}Rule to remember:
- Upcasting goes child to parent. Always safe. No cast needed.
- Downcasting goes parent to child. Risky. Check with
instanceoffirst.
⚖️ Two kinds: runtime vs compile-time
The two kinds side by side:
- Runtime polymorphism: from overriding. A child redefines a parent method. Java picks the version while the program runs, by the actual object. Needs inheritance or an interface. This is the powerful kind.
- Compile-time polymorphism: from overloading. Several methods, same name, different parameters. Java picks while compiling, by the arguments. No inheritance needed.
Here is overloading: same name print, chosen by the argument type.
class Printer { void print(int x) { System.out.println("Number: " + x); } void print(String x) { System.out.println("Text: " + x); }}
public class Main { public static void main(String[] args) { Printer p = new Printer(); p.print(42); // picks print(int) at compile time p.print("hello"); // picks print(String) at compile time }}Output
Number: 42Text: helloOverriding decides late, by the object. Overloading decides early, by the arguments. Both let one name mean many behaviors.
⚠️ Common Mistakes
Watch for these:
-
❌ Confusing overloading with overriding. Overloading is same name, different parameters. Overriding is same name, same parameters, in a child redefining the parent. Only overriding gives runtime polymorphism.
-
❌ Thinking the variable type picks the method. The object picks.
Animal a = new Dog(); a.makeSound();runs Dog’s version even thoughais declaredAnimal. -
❌ Getting the
@Overridesignature wrong. A slightly different parameter list is not an override; it becomes a separate overloaded method and the parent version still runs. Always write@Overrideso the compiler catches the mismatch.
// ❌ wrong: different parameter, this does NOT override@Overridevoid makeSound(String mood) { ... } // compiler error with @Override
// ✅ right: exact same signature as the parent@Overridevoid makeSound() { ... }- ❌ Downcasting blindly and getting a
ClassCastException. Casting aCatreference toDogcrashes at runtime. Always guard a downcast withinstanceoffirst.
Animal a = new Cat();
// ❌ crashes with ClassCastException, a is not a DogDog d = (Dog) a;
// ✅ check first, then castif (a instanceof Dog d2) { d2.fetch();}- ❌ Calling a child-only method through a parent reference.
Animal a = new Dog(); a.fetch();will not compile, because theAnimaltype does not know aboutfetch. You must downcast toDogfirst.
✅ Best Practices
Habits that keep polymorphic code clean and safe:
- Program to the parent type. Declare variables and parameters as
AnimalorShape, so your code works for any subtype, even later ones. - Always write
@Override. It catches signature mistakes early at no cost. - Prefer overriding over
instanceofchains. Let the object decide its own behavior. - Guard every downcast with
instanceof. Check first, then cast, so you never hit aClassCastException. - Keep overridden behavior consistent with the parent’s intent. A child’s
area()should still return an area.
🧩 What You’ve Learned
Quick recap:
- ✅ Polymorphism means one method call can take many forms of behavior depending on the actual object.
- ✅ A parent reference can hold a child object, like
Animal a = new Dog(). This is upcasting and it is always safe. - ✅ With runtime polymorphism (overriding), Java runs the actual object’s version of the method. This is dynamic dispatch.
- ✅ It lets you treat different objects uniformly, like calling
makeSoundon a whole array of animals orarea()on aShape[]. - ✅ Downcasting goes parent to child and is risky, so check with
instanceofbefore you cast. - ✅ Overriding gives runtime polymorphism; overloading gives compile-time polymorphism.
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
What does polymorphism allow?
Why: Polymorphism lets one method call produce different behavior based on the actual object.
- 2
For Animal a = new Dog(); a.makeSound();, which version runs?
Why: Runtime polymorphism runs the actual object's version, which is Dog's.
- 3
Which feature gives runtime polymorphism?
Why: Overriding a parent method in a child gives runtime polymorphism.
- 4
Why is programming to the parent type useful?
Why: Using the parent type lets one loop or method handle all current and future subtypes without changes.
🚀 What’s Next?
Polymorphism and inheritance lean on the idea of a general parent type. Sometimes that parent should only describe what children must do, without giving full details itself. That idea is abstraction, and it is the next concept. Let’s learn it.