Java Polymorphism

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 hand
if (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 if chain.

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 fine

Here is what happens when you call a.makeSound():

  • a is declared Animal, 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 a1 and a2 are declared Animal.
  • a1 really holds a Dog, a2 really holds a Cat.
  • Java picks makeSound by the actual object, so a1 says “Woof” and a2 says “Meow”.
  • Type is Animal both times, but the behavior belongs to the child. That is runtime polymorphism.

Output

Woof
Meow

Notice @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 makeSound on each, and every object answers with its own version.
  • We never ask “is this a Dog?”.
  • Add a Cow class tomorrow and the loop handles it with zero changes. That flexibility is why polymorphism is loved.

Output

Woof
Meow
Some sound

This 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:

  • s is just a Shape; 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.566370614359172
Area: 12.0
Area: 3.141592653589793
Total area: 27.707963227948964

Write 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 automatic
a.makeSound(); // still runs Dog's version

Downcasting 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 Dog
d.fetch(); // ✅ now we can call Dog-only methods

If 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 instanceof first.

⚖️ 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: 42
Text: hello

Overriding 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 though a is declared Animal.

  • ❌ Getting the @Override signature wrong. A slightly different parameter list is not an override; it becomes a separate overloaded method and the parent version still runs. Always write @Override so the compiler catches the mismatch.

// ❌ wrong: different parameter, this does NOT override
@Override
void makeSound(String mood) { ... } // compiler error with @Override
// ✅ right: exact same signature as the parent
@Override
void makeSound() { ... }
  • ❌ Downcasting blindly and getting a ClassCastException. Casting a Cat reference to Dog crashes at runtime. Always guard a downcast with instanceof first.
Animal a = new Cat();
// ❌ crashes with ClassCastException, a is not a Dog
Dog d = (Dog) a;
// ✅ check first, then cast
if (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 the Animal type does not know about fetch. You must downcast to Dog first.

✅ Best Practices

Habits that keep polymorphic code clean and safe:

  • Program to the parent type. Declare variables and parameters as Animal or Shape, so your code works for any subtype, even later ones.
  • Always write @Override. It catches signature mistakes early at no cost.
  • Prefer overriding over instanceof chains. Let the object decide its own behavior.
  • Guard every downcast with instanceof. Check first, then cast, so you never hit a ClassCastException.
  • 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 makeSound on a whole array of animals or area() on a Shape[].
  • Downcasting goes parent to child and is risky, so check with instanceof before 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. 1

    What does polymorphism allow?

    Why: Polymorphism lets one method call produce different behavior based on the actual object.

  2. 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. 3

    Which feature gives runtime polymorphism?

    Why: Overriding a parent method in a child gives runtime polymorphism.

  4. 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.

Java Abstraction

Share & Connect