Polymorphism is the ability to treat objects of different types as one type, and to have the right behaviour happen automatically when you use them. It is the payoff for the class, constructor and inheritance work that comes before it: instead of a growing pile of type checks, you get one method that works for every subtype.
What is polymorphism, in one sentence?
Write the code against the parent type, let each object decide for itself what the shared operation actually does. A shelter knows it is dealing with animals; a report generator knows it is dealing with exporters; a payment screen knows it is dealing with payment methods. None of them need to know the concrete classes in advance.
Polymorphism needs one ingredient: a common parent type that the different classes share. That parent can be a class or an interface, and it does not have to be a close relative of the objects. What matters is that the code can be written against the parent and still get the right behaviour at runtime.
Why store a list of the base type?
Because a list typed as the parent can hold children of any kind, and every element answers the shared methods in its own way. This is the shape most real programs end up with.
import java.util.List;
public class Demo {
static abstract class Animal {
private final String name;
Animal(String name) {
this.name = name;
}
String name() {
return name;
}
abstract String sound();
abstract String diet();
}
static class Dog extends Animal {
Dog(String name) {
super(name);
}
@Override
String sound() {
return "woof";
}
@Override
String diet() {
return "kibble";
}
}
static class Cat extends Animal {
Cat(String name) {
super(name);
}
@Override
String sound() {
return "meow";
}
@Override
String diet() {
return "fish";
}
}
static class Parrot extends Animal {
Parrot(String name) {
super(name);
}
@Override
String sound() {
return "squawk";
}
@Override
String diet() {
return "seeds";
}
}
public static void main(String[] args) {
List<Animal> shelter = List.of(new Dog("Rex"), new Cat("Mia"), new Parrot("Kiwi"));
for (Animal animal : shelter) {
System.out.println(animal.name() + " says " + animal.sound() + " and eats " + animal.diet());
}
}
}Three lines of output, one loop. The loop never mentions Dog, Cat or Parrot: it calls sound() and diet() on each element and gets an answer that belongs to that particular animal. That is the whole trick, and it is why the list is declared as List<Animal> rather than as three separate lists.
The list implementations differ on purpose. List.of builds a fixed set of items that cannot be added to or removed from, which suits a small hand-written collection. new ArrayList<>() is the one to reach for when the program builds the collection as it goes. The declared type stays List either way, and that declared type is the part that carries the polymorphism.
How do you write a method that works for every subtype?
Take the parent type as the parameter. That single decision is what allows one method to serve a dog, a cat and a parrot, and it is also what lets you add a new animal later without editing anything.
import java.util.List;
public class Demo {
static abstract class Animal {
private final String name;
Animal(String name) {
this.name = name;
}
String name() {
return name;
}
abstract String sound();
}
static class Dog extends Animal {
Dog(String name) {
super(name);
}
@Override
String sound() {
return "woof";
}
}
static class Cat extends Animal {
Cat(String name) {
super(name);
}
@Override
String sound() {
return "meow";
}
}
static void announce(Animal animal) {
System.out.println(animal.name() + " says " + animal.sound());
}
public static void main(String[] args) {
announce(new Dog("Rex"));
announce(new Cat("Mia"));
List<Animal> shelter = List.of(new Dog("Bolt"), new Cat("Tom"));
for (Animal animal : shelter) {
announce(animal);
}
}
}The announce method has no idea which concrete type it was handed, and it does not need one. The four output lines show it working for direct calls and for a loop over a list. If you added a Goose class tomorrow, announce would keep working unchanged, because the method only depends on the contract in the parent class.
What is the alternative to polymorphism?
Type checks. You can write the same feature by asking what each object is, and it compiles and works, but the method now knows every type in the program and has to be edited whenever a new one appears.
import java.util.List;
public class Demo {
static abstract class Animal {
private final String name;
Animal(String name) {
this.name = name;
}
String name() {
return name;
}
}
static class Dog extends Animal {
Dog(String name) {
super(name);
}
}
static class Cat extends Animal {
Cat(String name) {
super(name);
}
}
static class Parrot extends Animal {
Parrot(String name) {
super(name);
}
}
static String describe(Animal animal) {
if (animal instanceof Dog dog) {
return "dog " + dog.name();
} else if (animal instanceof Cat cat) {
return "cat " + cat.name();
} else if (animal instanceof Parrot parrot) {
return "parrot " + parrot.name();
}
return "unknown animal";
}
public static void main(String[] args) {
List<Animal> shelter = List.of(new Dog("Rex"), new Cat("Mia"), new Parrot("Kiwi"));
for (Animal animal : shelter) {
System.out.println(describe(animal));
}
}
}The output reads dog Rex, cat Mia and parrot Kiwi, and the method is perfectly reasonable. The problem shows up later: add a Goose class, and this method falls through to unknown animal until somebody remembers to edit it. The type-check version also grows a branch per class, so the list of what needs changing is exactly the list of things that can break.
Use instanceof when the behaviour genuinely differs per type and there are only a few types. Use polymorphism when there are many types, or when you want the compiler to remind you about the ones you have not handled yet.
What does a real program look like with polymorphism?
Here is the same shelter doing slightly more work: counting meals by type, and asking a specific question about one animal. The loop still never mentions a concrete class.
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class Demo {
static abstract class Animal {
private final String name;
Animal(String name) {
this.name = name;
}
String name() {
return name;
}
abstract String diet();
}
static class Dog extends Animal {
Dog(String name) {
super(name);
}
@Override
String diet() {
return "kibble";
}
}
static class Cat extends Animal {
Cat(String name) {
super(name);
}
@Override
String diet() {
return "fish";
}
}
static class Parrot extends Animal {
Parrot(String name) {
super(name);
}
@Override
String diet() {
return "seeds";
}
}
public static void main(String[] args) {
List<Animal> shelter = new ArrayList<>();
shelter.add(new Dog("Rex"));
shelter.add(new Cat("Mia"));
shelter.add(new Parrot("Kiwi"));
shelter.add(new Dog("Bolt"));
Map<String, Integer> meals = new TreeMap<>();
for (Animal animal : shelter) {
meals.merge(animal.diet(), 1, Integer::sum);
}
System.out.println(meals);
Animal first = shelter.get(0);
if (first instanceof Dog dog) {
System.out.println(dog.name() + " is a dog");
}
System.out.println("animals in shelter: " + shelter.size());
}
}The output is {fish=1, kibble=2, seeds=1}, then Rex is a dog, then animals in shelter: 4. Note the mix: the meal count comes from the polymorphic diet() call, while the "is a dog" line is a deliberate one-off check on a single element, where a type-specific detail such as a breed would live. Good code uses both tools and does not force one where the other fits better.
The design pays off when the shelter takes in something new. Add a Rabbit class with its own diet(), drop it into the list, and the meal count, the printed output and the animal count all keep working. Nothing in the existing code needed an edit, which is the practical meaning of the idea that a design should be open for extension and closed for modification.
Where does polymorphism stop paying?
- When there is only ever one subtype, the abstraction costs a level of indirection for nothing.
- When subtypes share no behaviour, an interface with a single method is often enough.
- When the variations are about data rather than behaviour, a switch or a lookup table is clearer.
- When a hierarchy grows several levels deep to describe a small domain, composition usually reads better.
- When you need a type-specific detail for every item, a type check may genuinely be the simpler choice.
Try it yourself: build a notification system. Write an abstract class or interface for a channel with a send(String message) operation, then implement three channels: email, SMS and push. Store them in a list typed as the parent and send the same message to every one, printing what each channel does. Add a fourth channel, a webhook that posts to a URL, and confirm you do not have to touch the sending loop at all. Then add a second shared behaviour, such as a label() that each channel supplies, and check whether a default method or an abstract parent method fits better in your design.