Polymorphism in Java: One Method, Many Forms Explained Simply
Learn polymorphism in Java the easy way — the third pillar of OOP. See how the same method call behaves differently per object, with dynamic dispatch explained by example.
Polymorphism is a big, intimidating-looking word — but the idea behind it is beautifully simple. It comes from Greek: "poly" (many) + "morph" (forms), so it means "many forms." In Java, it's the ability for the same method call to behave differently depending on the object it's acting on. It's the third pillar of OOP, and it builds directly on the inheritance and overriding you just learned.
The core idea
Think of a single command: "make a sound." Give that command to a dog and you get a bark; give it to a cat and you get a meow; give it to a cow and you get a moo. Same command, different results depending on the animal. That's polymorphism — one interface, many behaviours.
Setting it up with overriding
We start with a parent class and let each child override a method its own way:
class Animal { void makeSound() { System.out.println("Some generic sound"); } } class Dog extends Animal { @Override void makeSound() { System.out.println("Woof!"); } } class Cat extends Animal { @Override void makeSound() { System.out.println("Meow!"); } }
The magic: a parent type holding a child object
Here's the key move. Because a Dog is an Animal, you're allowed to store a Dog object in an Animal variable. And when you call the method, Java runs the actual object's version — not the variable's type version:
Animal a1 = new Dog(); // an Animal variable holding a Dog Animal a2 = new Cat(); // an Animal variable holding a Cat a1.makeSound(); // "Woof!" — runs Dog's version a2.makeSound(); // "Meow!" — runs Cat's version
Even though both variables are typed Animal, each call dispatches to the real object's overridden method. Java figures out the correct method at runtime — this is called dynamic dispatch, and it's the engine behind polymorphism.
Why this is powerful: one loop for everything
The payoff is code that works with a whole family of types without knowing or caring which specific one it has. Watch how one loop handles every animal:
Animal[] zoo = { new Dog(), new Cat(), new Dog() };
for (Animal animal : zoo) {
animal.makeSound(); // Woof! Meow! Woof! — each does its own thing
}
Add a new Cow class tomorrow and this loop needs zero changes — it already handles it. That flexibility is why polymorphism sits at the heart of good software design.
Two flavours of polymorphism (a note)
| Type | What it means |
|---|---|
| Runtime (overriding) | Same method, different classes — the main kind, shown above |
| Compile-time (overloading) | Same method name, different parameters in one class (e.g. add(int,int) vs add(double,double)) |
You've actually met overloading already — with constructors. When people say "polymorphism," they usually mean the runtime/overriding kind, because that's where the real design power lies.
Polymorphism was the concept that finally made OOP feel elegant to me rather than just organised. The first time I wrote a single loop that correctly handled a dozen different object types — each behaving in its own way, with no giant if-else chain — it genuinely felt like magic. It's the idea that lets large programs stay flexible as they grow, and you'll see it everywhere in professional Java and frameworks like Spring.
Key takeaways
- Polymorphism = "many forms": the same method call behaves differently per object.
- A parent-type variable can hold a child object; the object's overridden method runs (dynamic dispatch).
- This lets one piece of code (like a loop) work across a whole family of types.
- Runtime polymorphism uses overriding; overloading is the compile-time cousin.
← Previous: Lesson 15 — Inheritance in Java
Next: Lesson 17 — Abstraction & Interfaces →