7.3 Polymorphism
Polymorphism is one of the most powerful features of object-oriented programming, allowing a single reference variable to refer to objects of multiple types. Thanks to polymorphism, code becomes flexible and resilient to change. In Java, there are two key concepts to understand:
- Method Overriding: Redefining a parent class's method to suit the child class
- Reference Variable Type Casting (Upcasting / Downcasting): Using a parent-type variable to point to a child-type object
1. Method Overriding
As established in inheritance, a child class inherits the parent's methods. But what if the inherited behavior doesn't suit the child's characteristics? In that case the child can redefine the inherited method to fit itself. This is called overriding.
class Animal {
void cry() {
System.out.println("The animal makes a sound.");
}
}
class Dog extends Animal {
// Override the parent's cry() method
@Override
void cry() {
System.out.println("Woof woof!");
}
}
class Cat extends Animal {
// Override the parent's cry() method
@Override
void cry() {
System.out.println("Meow~");
}
}
Rules of Overriding
- The method name, parameters, and return type must be exactly identical to those in the parent class.
- The access modifier of the overriding method cannot be more restrictive than the parent's. (e.g., if the parent is
public, the child cannot narrow it toprivate.) - The
@Overrideannotation is strongly recommended — it tells the compiler to check that you actually overrode a method correctly, preventing typo mistakes.
2. Upcasting, Downcasting, and Dynamic Binding
The true power of polymorphism appears when a parent-type variable holds a reference to a child-type object.
// A parent-type reference variable can point to a child object!
Animal myDog = new Dog(); // upcasting
Animal myCat = new Cat();
This works because Dog IS an Animal, and Cat IS an Animal (the IS-A relationship holds). This implicit conversion is called Upcasting and can be done automatically.
Conversely, "An Animal is a Dog" does not always hold. To assign a parent-type reference to a child variable you must explicitly cast with parentheses (Type). This is called Downcasting.
Flexible Code with Polymorphism
public class PolymorphismExample {
public static void main(String[] args) {
// Manage various child objects with a single parent-type array
Animal[] animals = new Animal[3];
animals[0] = new Dog();
animals[1] = new Cat();
animals[2] = new Animal();
// Same command, different behavior based on actual type (Dynamic Binding)
for (Animal a : animals) {
a.cry();
}
}
}
[Output]
Woof woof!
Meow~
The animal makes a sound.
We placed both Dogs and Cats into an Animal array. The same a.cry() call produces different results depending on the actual object type — this is dynamic binding. Without polymorphism, you would need separate arrays and loops for each type. When a new animal Cow is added, just drop it into the array without modifying existing code.
3. instanceof and Pattern Matching
Before downcasting, always verify the actual type to avoid ClassCastException.
public class DowncastingExample {
public static void main(String[] args) {
Animal[] animals = { new Dog(), new Cat(), new Animal() };
for (Animal a : animals) {
// Classic instanceof check (Java 1.0+)
if (a instanceof Dog) {
Dog dog = (Dog) a; // safe downcast
dog.fetch();
} else if (a instanceof Cat) {
Cat cat = (Cat) a; // safe downcast
cat.purr();
} else {
a.cry();
}
}
System.out.println("--- Pattern Matching (Java 16+) ---");
for (Animal a : animals) {
// Pattern matching instanceof — no explicit cast needed
if (a instanceof Dog dog) {
dog.fetch();
} else if (a instanceof Cat cat) {
cat.purr();
} else {
a.cry();
}
}
}
}
4. Polymorphism with Interfaces
Polymorphism works equally well through interfaces.
interface Shape {
double area();
void draw();
}
class Circle implements Shape {
private double radius;
public Circle(double radius) { this.radius = radius; }
@Override
public double area() { return Math.PI * radius * radius; }
@Override
public void draw() { System.out.println("Drawing Circle (r=" + radius + ")"); }
}
class Rectangle implements Shape {
private double width, height;
public Rectangle(double w, double h) { this.width = w; this.height = h; }
@Override
public double area() { return width * height; }
@Override
public void draw() { System.out.println("Drawing Rectangle (" + width + "x" + height + ")"); }
}
class Triangle implements Shape {
private double base, height;
public Triangle(double b, double h) { this.base = b; this.height = h; }
@Override
public double area() { return 0.5 * base * height; }
@Override
public void draw() { System.out.println("Drawing Triangle (b=" + base + ", h=" + height + ")"); }
}
Shape Calculator Example
public class ShapeCalculator {
// Works with ANY Shape implementation — polymorphism in action
public static void printShapeInfo(Shape shape) {
shape.draw();
System.out.printf(" Area: %.2f%n", shape.area());
}
public static double totalArea(Shape[] shapes) {
double total = 0;
for (Shape s : shapes) {
total += s.area(); // polymorphic call
}
return total;
}
public static Shape largestShape(Shape[] shapes) {
Shape largest = shapes[0];
for (Shape s : shapes) {
if (s.area() > largest.area()) {
largest = s;
}
}
return largest;
}
public static void main(String[] args) {
Shape[] shapes = {
new Circle(5.0),
new Rectangle(4.0, 6.0),
new Triangle(3.0, 8.0),
new Circle(2.5),
new Rectangle(10.0, 3.0)
};
System.out.println("=== Shape Report ===");
for (Shape s : shapes) {
printShapeInfo(s);
}
System.out.printf("%nTotal area: %.2f%n", totalArea(shapes));
Shape largest = largestShape(shapes);
System.out.print("Largest shape: ");
largest.draw();
System.out.printf(" Area: %.2f%n", largest.area());
}
}
Output:
=== Shape Report ===
Drawing Circle (r=5.0)
Area: 78.54
Drawing Rectangle (4.0x6.0)
Area: 24.00
Drawing Triangle (b=3.0, h=8.0)
Area: 12.00
Drawing Circle (r=2.5)
Area: 19.63
Drawing Rectangle (10.0x3.0)
Area: 30.00
Total area: 164.17
Largest shape: Drawing Circle (r=5.0)
Area: 78.54
5. Polymorphism and the Open/Closed Principle
Polymorphism is the key to following the Open/Closed Principle(OCP) — open for extension, closed for modification.
interface Notification {
void send(String message);
}
class EmailNotification implements Notification {
private String email;
public EmailNotification(String email) { this.email = email; }
@Override
public void send(String message) {
System.out.println("[Email to " + email + "]: " + message);
}
}
class SmsNotification implements Notification {
private String phone;
public SmsNotification(String phone) { this.phone = phone; }
@Override
public void send(String message) {
System.out.println("[SMS to " + phone + "]: " + message);
}
}
class PushNotification implements Notification {
private String deviceId;
public PushNotification(String deviceId) { this.deviceId = deviceId; }
@Override
public void send(String message) {
System.out.println("[Push to " + deviceId + "]: " + message);
}
}
// No changes to this class even when new notification types are added!
class NotificationService {
private List<Notification> channels = new ArrayList<>();
public void addChannel(Notification channel) {
channels.add(channel);
}
public void notifyAll(String message) {
for (Notification n : channels) {
n.send(message); // polymorphic call
}
}
}
public class NotificationDemo {
public static void main(String[] args) {
NotificationService service = new NotificationService();
service.addChannel(new EmailNotification("user@example.com"));
service.addChannel(new SmsNotification("+1-555-1234"));
service.addChannel(new PushNotification("device-abc-123"));
service.notifyAll("Your order has been shipped!");
}
}
Output:
[Email to user@example.com]: Your order has been shipped!
[SMS to +1-555-1234]: Your order has been shipped!
[Push to device-abc-123]: Your order has been shipped!
Summary
| Concept | Core Idea |
|---|---|
| Method Overriding | Child redefines parent method; @Override annotation recommended |
| Upcasting | Implicit; child object stored in parent-type variable |
| Downcasting | Explicit cast (ChildType) back to child; verify with instanceof first |
| Dynamic Binding | At runtime the actual object type determines which method runs |
| Pattern Matching | Java 16+ instanceof Dog dog — combines check and cast |
| Interface Polymorphism | Unrelated classes share a common interface type |
Polymorphism is not just a language feature — it is an architectural tool. Designing around interfaces and abstract types (instead of concrete classes) keeps your codebase flexible. When requirements change, you add new implementations rather than editing existing ones. This is the essence of professional software design.