Polymorphism
One message, many forms — how a single method call can invoke many different behaviors depending on the actual receiver
The Greek roots of polymorphism literally mean "many shapes." In OOP, the word means: the same message, sent through the same variable type, can produce different behavior at runtime depending on what kind of object is actually receiving it.
This is what makes the lines you wrote on the previous two pages so
powerful. When Main loops over a list of Noisy and sends each
one sound(), the JVM dispatches to the actual class of each
object — not the variable's declared type.
Two-level thinking: compile-time vs. runtime types
Every reference variable in Java has two types associated with it at any moment:
- Compile-time type (also called static type or declared type): the type written in the variable declaration. The compiler uses it to decide which method names are even allowed.
- Runtime type (also called dynamic type or actual class): the class of the object the variable currently points to. The JVM uses it to decide which override of a method to actually run.
The compiler only allows a.speak() because Animal has a method
called speak(). The JVM, at runtime, sees that a actually points
to a Dog and runs Dog.speak(). This is sometimes called dynamic
dispatch or virtual dispatch, and in Java all instance
methods work this way by default.
Why polymorphism is the payoff of OOP
Polymorphism is what makes the previous chapters worth the effort. Encapsulation, inheritance, and interfaces all build up to this moment: a function that processes any number of different concrete types as long as they share the right interface.
That last node — the new shape added next year — is the whole
point. You can ship renderAll today, and someone can plug in a
brand new Hexagon tomorrow, and your function doesn't change.
Renderer.drawAll knows nothing about circles, squares, or
triangles. It depends only on Shape. The diversity is real — each
shape has its own internal state and computes its own representation
— but the interaction with Renderer is uniform.
Subtype polymorphism vs. ad-hoc polymorphism
There are several flavors of polymorphism. In a beginner OOP course the one to know is subtype polymorphism, which is what we just saw: a variable of a parent type can hold any subtype, and calls dispatch to the actual subtype.
You met overloading earlier in Methods and Messages. You will meet generics in passing throughout this course. All three are "polymorphism" in their broad sense, but when designers say "the power of polymorphism" they almost always mean subtype polymorphism.
Upcasting and downcasting
Going from a subtype to a supertype is automatic and always safe — it's called upcasting:
Dog d = new Dog("Rex");
Animal a = d; // upcast, no syntax needed — every Dog is an AnimalGoing the other way — claiming a parent-typed reference is actually a particular subtype — is called downcasting. It requires a cast expression and may fail at runtime if you're wrong.
A heavy reliance on instanceof and downcasting is usually a smell
— it often means the supertype is missing an abstraction. If you find
yourself writing many if (x instanceof A) ... else if (x instanceof B) ... blocks, ask whether the parent type should grow a method that
each subtype overrides.
A bigger payoff: open–closed
There is a famous design idea called the open–closed principle: software should be open for extension but closed for modification. Polymorphism is what makes that achievable.
You extend the system by adding new classes that implement the existing interfaces or abstract classes. The existing code, which already worked and was already tested, doesn't have to change.
Practice
Implement a tiny pay system using interface-based polymorphism.
Payableinterface with a single methodint monthlyPayCents().SalariedEmployeeimplementsPayable. Constructor takes monthly salary in cents;monthlyPayCents()returns it directly.HourlyEmployeeimplementsPayable. Constructor takesint hourlyRateCentsandint hoursWorked;monthlyPayCents()returnshourlyRateCents * hoursWorked.Payrollhas a methodint totalCents(List<Payable> payees)that sums the monthly pay of all payees, no matter the concrete class.
The provided Main exercises a small list. Expected output:
total cents: 800000
Test your understanding
Given Animal a = new Dog(); and a.speak();, which method actually runs?
Animal.speak() because a's declared type is Animal
Neither — the compiler rejects this code
Dog.speak() because the runtime type of the object a points to is Dog
It is undefined; the JVM picks at random
Why is polymorphism considered the "payoff" of OOP?
Because it makes programs use less memory
Because it removes the need for any interfaces or abstract classes
Because one piece of generic code can handle any number of concrete types, including types that don't exist yet
Because it allows downcasting without checks
Which of these is a sign you might be underusing polymorphism?
You have classes that implement an interface
You override equals and hashCode on your value classes
Several methods in your code do if (x instanceof A) ... else if (x instanceof B) ... and call different code per branch
Your interfaces have only one method