Polymorphism
Polymorphism literally means "many forms." In Java, it describes a situation where the same method call behaves differently depending on the actual type of the object it's called on. It's one of the core pillars of object-oriented programming, alongside encapsulation, abstraction, and inheritance.
Two Kinds of Polymorphism
| Type | How it works | Resolved |
|---|---|---|
| Runtime polymorphism | Method overriding — a subclass provides its own version of a method already defined in its superclass | At runtime, based on the actual object |
| Compile-time polymorphism | Method overloading — multiple methods with the same name but different parameters | At compile time, based on the arguments |
Runtime Polymorphism (Method Overriding)
Recall the Animal, Dog, and Cat example from the inheritance lesson. Each subclass
overrides a speak() method defined in Animal. If we store different animals in a single
List<Animal> and call speak() on each one, Java picks the correct overridden version at
runtime — based on what the object actually is, not the reference type used to hold it.
Example: Same Call, Different Behavior
class Animal {
public void speak() {
System.out.println("The animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void speak() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
@Override
public void speak() {
System.out.println("Meow!");
}
}
public class PolymorphismDemo {
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
animals.add(new Dog());
animals.add(new Cat());
for (Animal a : animals) {
a.speak(); // Dog prints "Woof!", Cat prints "Meow!"
}
}
}
What's happening:
- Every element in the list is referenced as an
Animal, but each one is actually aDogor aCat. - Calling
a.speak()runs the version ofspeak()that belongs to the real object, not theAnimalversion. - This lets you write code that works with the general
Animaltype while still getting specific behavior for each subclass — noif/elsechains checking types needed.
Upcasting
Treating a subclass object as its superclass type — like assigning a Dog to an Animal reference —
is called upcasting. It happens automatically and safely, because a Dog is an
Animal. Upcasting is exactly what makes the list example above possible: the list only needs to know about
Animal, yet it can hold any subclass of it.
Animal a = new Dog(); // upcasting: Dog treated as an Animal
a.speak(); // still calls Dog's overridden speak()
a is declared as type Animal, only Animal's methods are visible on it directly. But when an overridden method is called, Java still runs the Dog version — this is the essence of runtime polymorphism.Compile-Time "Polymorphism" (Method Overloading)
Method overloading is often called compile-time polymorphism because the compiler decides which method to call based purely on the number and types of arguments — before the program ever runs. It's a different mechanism from overriding, but it shares the same "one name, many behaviors" spirit.
class Calculator {
public int add(int x, int y) { return x + y; }
public double add(double x, double y) { return x + y; }
}
Both methods are named add, but the compiler picks the right one based on whether you pass ints
or doubles. We covered this in more depth in the Methods lesson — it's mentioned here just to contrast it
with runtime polymorphism.