Polymorphism
Polymorphism — 'many forms' — allows different objects to respond to the same method call in their own appropriate way. It is the mechanism that makes code extensible without modification.
What Polymorphism Means in Practice
You have an array of Person objects — some are Learners, some are Facilitators. You call introduce() on each. Without polymorphism, you would need to check each object's type and call a different method. With polymorphism, you call the same method and Java automatically uses the correct version for each object. This is runtime (dynamic) dispatch.
Method Overriding
Overriding means a subclass defines a method with the same name, same parameter list, and same return type as a parent method. At runtime, Java looks at the actual type of the object and calls the most specific version. The @Override annotation asks the compiler to verify you are actually overriding something — a typo in the method name would otherwise silently create a new method instead of overriding.
public class Person {
protected String name;
public Person(String name) { this.name = name; }
public void introduce() {
System.out.println("Hello, I am " + name + ".");
}
}
public class Learner extends Person {
private String course;
public Learner(String name, String course) {
super(name); this.course = course;
}
@Override
public void introduce() {
System.out.println("I am " + name + ", studying " + course + " at Your IT Tutor.");
}
}
public class Facilitator extends Person {
private String specialisation;
public Facilitator(String name, String spec) {
super(name); this.specialisation = spec;
}
@Override
public void introduce() {
System.out.println("I am " + name + ", a facilitator specialising in " + specialisation + ".");
}
}
Polymorphism in Action
// A Learner IS-A Person. A Facilitator IS-A Person.
// So we can store both in a Person array:
Person[] people = {
new Learner("Thandi", "Java"),
new Learner("Sipho", "Python"),
new Facilitator("Ms Dube", "Software Development"),
new Learner("Lerato", "SQL"),
new Facilitator("Mr Khumalo", "Data Engineering")
};
// One loop, one method call — Java selects the right version for each object:
for (Person p : people) {
p.introduce();
}
// Output:
// I am Thandi, studying Java at Your IT Tutor.
// I am Sipho, studying Python at Your IT Tutor.
// I am Ms Dube, a facilitator specialising in Software Development.
// I am Lerato, studying SQL at Your IT Tutor.
// I am Mr Khumalo, a facilitator specialising in Data Engineering.
Casting and instanceof
When you hold an object through a parent-type reference, you can only call parent-type methods. To access subclass-specific methods, you must cast down — but first check with instanceof to avoid a ClassCastException.
for (Person p : people) {
p.introduce(); // always works
// Access Learner-specific methods:
if (p instanceof Learner) {
Learner l = (Learner) p; // downcast — safe because we checked
System.out.println(" Course: " + l.getCourse());
} else if (p instanceof Facilitator) {
Facilitator f = (Facilitator) p;
System.out.println(" Specialisation: " + f.getSpecialisation());
}
}
// Java 16+ pattern matching instanceof (cleaner):
if (p instanceof Learner l) {
System.out.println(l.getCourse()); // l is already cast — no explicit cast needed
}
Overriding equals() and hashCode()
Inherited from Object, the default equals() compares references (same object in memory). Override it when two objects should be considered equal based on their data.
@Override
public boolean equals(Object obj) {
if (this == obj) return true; // same reference
if (!(obj instanceof Learner)) return false; // different type
Learner other = (Learner) obj;
return this.name.equals(other.name) && this.course.equals(other.course);
}
// Always override hashCode when overriding equals:
@Override
public int hashCode() {
return java.util.Objects.hash(name, course);
}
Learner a = new Learner("Thandi", "Java");
Learner b = new Learner("Thandi", "Java");
System.out.println(a == b); // false — different objects
System.out.println(a.equals(b)); // true — same data
Practice Task
Your Turn
Create a Shape hierarchy: Shape (colour, getArea(), getPerimeter(), describe()), Circle (radius), Rectangle (width, height), Triangle (base, height, hypotenuse). Override getArea(), getPerimeter(), and describe() in each subclass. Store all three in a Shape[], loop through, and for each: print its colour, area, perimeter. Also print the total combined area of all shapes.
Common Mistakes
- Forgetting
@Override— without it, a typo creates a new method instead of overriding. The annotation makes the compiler catch this. - Overriding with a more restrictive access modifier — you can widen access (protected → public) but not narrow it.
- Overriding a
privatemethod — private methods are not visible to subclasses; you are creating a new method, not overriding. - Overriding
equalswithout overridinghashCode— collections like HashMap rely on both being consistent. - Confusing overriding (same signature, subclass) with overloading (same name, different parameters, same class).
Professional Tip
Design your superclass methods with overriding in mind. If you know subclasses will need to specialise behaviour, make those methods overridable and give them sensible defaults. Polymorphism is what lets you add a new shape to the system without touching any existing code.
Mini Quiz
Which annotation should you always use when overriding a method?