Polymorphism
IB Syllabus: B3.2 (HL) – Implement polymorphism using parent-type references and method overriding.
HL Only – This page covers content assessed at HL level only.
Table of Contents
- Key Concepts
- Worked Examples
- Quick Check
- Trace Exercise
- Predict the Output
- Practice Exercises
- Connections
Key Concepts
What is Polymorphism?
Polymorphism means “many forms.” In OOP, it means that a single variable of a parent type can refer to objects of different child types, and when a method is called, Java automatically runs the correct version based on the actual object type – not the declared variable type.
Animal a1 = new Dog("Max", 3, "Labrador");
Animal a2 = new Cat("Whiskers", 5);
Animal a3 = new Animal("Generic", 1);
System.out.println(a1.speak()); // Woof! (Dog's version)
System.out.println(a2.speak()); // Meow! (Cat's version)
System.out.println(a3.speak()); // ... (Animal's version)
All three variables are declared as Animal, but each calls a different speak() method. Java determines which version to run at runtime based on the actual object – this is called dynamic dispatch or runtime polymorphism.
The key insight: a parent-type variable can hold any child-type object. When you call a method, Java runs the child’s overridden version if one exists. This is what makes polymorphism powerful – you write code that works with the general type, and it automatically adapts to specific types.
Polymorphic Arrays
The most powerful application of polymorphism is creating an array of the parent type that holds a mix of child objects:
Animal[] zoo = new Animal[4];
zoo[0] = new Dog("Max", 3, "Labrador");
zoo[1] = new Cat("Whiskers", 5);
zoo[2] = new Dog("Buddy", 2, "Beagle");
zoo[3] = new Cat("Luna", 4);
// One loop, handles ALL animal types
for (int i = 0; i < zoo.length; i++) {
System.out.println(zoo[i].getName() + " says " + zoo[i].speak());
}
Output:
Max says Woof!
Whiskers says Meow!
Buddy says Woof!
Luna says Meow!
The loop does not need to know whether each element is a Dog or a Cat. It calls speak() and Java dispatches to the correct version. If you later add a Bird class with speak() returning “Tweet!”, the loop works without any changes.
Why Polymorphism Matters
Without polymorphism, you would need separate arrays and separate loops for each animal type:
// WITHOUT polymorphism -- fragile and repetitive
Dog[] dogs = new Dog[10];
Cat[] cats = new Cat[10];
for (int i = 0; i < dogs.length; i++) {
if (dogs[i] != null) System.out.println(dogs[i].speak());
}
for (int i = 0; i < cats.length; i++) {
if (cats[i] != null) System.out.println(cats[i].speak());
}
// Add a Bird? Need a THIRD array and a THIRD loop.
With polymorphism:
// WITH polymorphism -- one array, one loop, works for ALL types
Animal[] animals = new Animal[30];
// Fill with any mix of Dogs, Cats, Birds...
for (int i = 0; i < animals.length; i++) {
if (animals[i] != null) {
System.out.println(animals[i].speak());
}
}
// Add a Bird? Just create Bird objects. The loop ALREADY works.
Advantages of polymorphism:
- Code reusability – one method or loop works for all subtypes
- Extensibility – add new child classes without modifying existing code
- Flexibility – the same variable can hold different object types at different times
- Simplicity – code is written for the general type, reducing complexity
How It Works: Compile Time vs Runtime
Animal a = new Dog("Max", 3, "Labrador");
a.speak(); // Which speak() runs?
// a.getBreed(); // Does this compile?
At compile time, Java checks the declared type (Animal). The compiler allows only methods that Animal defines. Since Animal has speak(), the call compiles. But Animal does NOT have getBreed(), so a.getBreed() would be a compile error – even though the actual object is a Dog.
At runtime, Java checks the actual object type (Dog). It finds that Dog overrides speak(), so it runs Dog.speak() – returning “Woof!” instead of “…”.
| What Java checks | Result | |
|---|---|---|
| Compile time | Declared type (Animal) | Only Animal methods are allowed |
| Runtime | Actual object type (Dog) | Overridden methods use the child’s version |
A parent-type variable can only call methods defined in the parent class. Even if the object is actually a Dog, you cannot call
getBreed()through anAnimalvariable. You would need to cast:((Dog) a).getBreed(). But casting should be used sparingly – it defeats the purpose of polymorphism.
The Racer Example
A classic example of polymorphism in action, using the class hierarchy from Inheritance:
Racer[] race = new Racer[2];
race[0] = new Hare("Harry");
race[1] = new Tortoise("Terry");
// Run 100 rounds
for (int round = 0; round < 100; round++) {
for (int i = 0; i < race.length; i++) {
race[i].move(); // each racer moves differently
}
}
// Check positions
for (int i = 0; i < race.length; i++) {
System.out.println(race[i].getName() + ": " + race[i].getPosition());
}
The Hare’s move() jumps 5 steps 30% of the time and sleeps 70% of the time. The Tortoise’s move() always advances 1 step. The loop calls move() on each – polymorphism ensures the right behaviour runs for each racer. Sometimes the tortoise wins.
Worked Examples
Example 1: Payment Processing
public class Employee {
private String name;
private double baseSalary;
public Employee(String name, double baseSalary) {
this.name = name;
this.baseSalary = baseSalary;
}
public String getName() { return name; }
public double calculatePay() {
return baseSalary;
}
}
public class Manager extends Employee {
private double bonus;
public Manager(String name, double baseSalary, double bonus) {
super(name, baseSalary);
this.bonus = bonus;
}
@Override
public double calculatePay() {
return super.calculatePay() + bonus;
}
}
public class SalesPerson extends Employee {
private double commissionRate;
private double totalSales;
public SalesPerson(String name, double baseSalary, double rate, double sales) {
super(name, baseSalary);
this.commissionRate = rate;
this.totalSales = sales;
}
@Override
public double calculatePay() {
return super.calculatePay() + (commissionRate * totalSales);
}
}
// Polymorphic array -- one type, three behaviours
Employee[] staff = new Employee[3];
staff[0] = new Employee("Alice", 50000);
staff[1] = new Manager("Bob", 60000, 10000);
staff[2] = new SalesPerson("Charlie", 40000, 0.05, 200000);
double totalPayroll = 0;
for (int i = 0; i < staff.length; i++) {
double pay = staff[i].calculatePay();
System.out.println(staff[i].getName() + ": $" + pay);
totalPayroll = totalPayroll + pay;
}
System.out.println("Total: $" + totalPayroll);
Output:
Alice: $50000.0
Bob: $70000.0
Charlie: $50000.0
Total: $170000.0
One loop handles all three employee types. Each calculatePay() call dispatches to the correct overridden version.
Quick Check
Q1. What does polymorphism allow?
Q2. Animal a = new Dog("Max", 3, "Lab"); -- can you call a.getBreed()?
Q3. Animal a = new Dog("Rex", 2, "Poodle"); -- what does a.speak() return?
Q4. A program processes an Animal[] array. A new Bird class is added. What changes are needed in the loop?
Trace Exercise
Trace the polymorphic method calls. Each Employee type has a different calculatePay().
Trace: Polymorphic Dispatch
Employee[] staff = new Employee[3];
staff[0] = new Employee("Alice", 50000);
staff[1] = new Manager("Bob", 60000, 10000);
staff[2] = new SalesPerson("Charlie", 40000, 0.05, 200000);| Index | Actual type | calculatePay() |
|---|---|---|
| 0 | ||
| 1 | ||
| 2 |
Total payroll:
SalesPerson Charlie: base 40000 + (0.05 * 200000) = 40000 + 10000 = 50000.
Predict the Output
What does this print?
Animal[] zoo = new Animal[2];
zoo[0] = new Dog("Max", 3, "Lab");
zoo[1] = new Cat("Luna", 4);
System.out.println(zoo[0].speak());
System.out.println(zoo[1].speak());Type both lines separated by \n:
Practice Exercises
Core
-
Polymorphic array – Create an
Animal[]with 2 Dogs and 2 Cats. Write a loop that prints each animal’s name and sound usinggetName()andspeak(). -
Compile time vs runtime – Given
Animal a = new Dog("Rex", 5, "Poodle");, state which of these compile and which don’t. For those that compile, state the output:- (a)
a.speak() - (b)
a.getName() - (c)
a.getBreed() - (d)
a.getAge()
- (a)
Extension
-
Payroll system – Using the Employee hierarchy from Example 1, create an array of 5 employees (mix of Employee, Manager, and SalesPerson). Write a method
printPayroll(Employee[] staff)that prints each person’s name and pay, then the total. -
Explain why – A student writes separate
if-elsechecks for each animal type instead of using polymorphism. Explain two problems with this approach and how polymorphism solves them.
Challenge
- Shape calculator – Create a
Shapeparent withgetArea()andgetPerimeter(). CreateCircle,Rectangle, andTrianglechildren that override both methods. Store a mix in aShape[]array and write a method that finds the shape with the largest area.
Connections
- Prerequisites: Inheritance – polymorphism requires inheritance
- Prerequisites: Method Overriding – overridden methods are what polymorphism dispatches
- Next: Abstract Classes – forcing subclasses to override methods
- Related: Aggregation – polymorphic arrays combine aggregation with inheritance