Same call, different behaviour depending on the real object
A Person reference can point at a Student or a Teacher, and calling the same method on it runs whichever version actually belongs to the real object underneath — decided at runtime, not by the reference's declared type.
After this lesson
You should be able to
- Override a method and explain why the object's real type decides which version runs.
- Write an abstract class with an abstract method and explain why it cannot be instantiated.
- State that every Java class ultimately extends Object, and name one method it provides.
The reference type is not the whole story
A variable declared as Person can hold a Student, a Teacher, or a plain Person — the declared type only limits which methods you are allowed to call through that variable, it does not fix which version of an overridden method actually runs. That decision is made at runtime by looking at the real object the reference points at, a mechanism called dynamic dispatch.
This is what makes polymorphism useful in practice: write a loop over an array of Person, call display() on each, and every object prints itself correctly as whatever it actually is — Student, Teacher, or plain Person — without the loop needing an if-chain checking each type. The loop stays simple no matter how many new subclasses are added later.
Person[] people = { new Person(), new Student(), new Teacher() };
for (Person p : people) {
p.display(); // runs Student's or Teacher's version automatically
}Abstract classes: a blueprint that refuses to be built directly
Sometimes a base class exists purely to define a shape that subclasses must fill in — a Shape class knows every shape needs an area() method, but has no sensible implementation of its own, since a generic "shape" has no formula. Marking the class and that method abstract makes this explicit: new Shape() will not compile, but new Circle() will, as long as Circle actually implements area().
This is the specification form of inheritance from the earlier lesson, made concrete: an abstract class is a contract, enforced by the compiler, that every concrete subclass must honour before it is even allowed to be built.
abstract class Shape {
abstract double area(); // no body — every subclass must supply one
}
class Circle extends Shape {
private double radius;
double area() {
return Math.PI * radius * radius;
}
}
// Shape s = new Shape(); // does not compile
// Shape s = new Circle(); // compiles — Circle fulfilled the contractEvery class you write already extends something
Even a class you declare with no extends clause at all silently extends Object, a class Java provides at the very top of every inheritance hierarchy. This is why every object you have ever created, including your very first Student, already has methods like toString() and equals() available — they come from Object, inherited the same way any other method would be.
This is why calling System.out.println(someObject) directly, without a display() method at all, still prints something — Object's default toString() runs, producing a not-very-readable class name and memory address. Overriding toString() in your own classes, which you will do increasingly from here on, is simply overriding a method you inherited from Object, using exactly the same override mechanism from earlier in this lesson.
Try it yourself
Make Shape's area() abstract as shown above, and add a second subclass, Rectangle, implementing area() differently. Write a loop over a Shape array containing one Circle and one Rectangle, printing each one's area — without any type-checking if statements.
Need a hint?
The loop body is identical to the Person example — call area() on the Shape reference and let dynamic dispatch pick the right version.
Check the worked solution
The loop needs no type checks at all, because dynamic dispatch already handles picking the correct area() for each real object — this is the exact same mechanism as the Person/Student loop, just with abstract classes instead of a concrete base class. This is the payoff abstract classes and polymorphism give together: correct behaviour, with code that stays simple as more shapes are added.
class Rectangle extends Shape {
private double length, width;
double area() {
return length * width;
}
}
Shape[] shapes = { new Circle(), new Rectangle() };
for (Shape s : shapes) {
System.out.println(s.area());
}Quick check
A Person array holds a Student object at index 0. Why does calling display() on that array element run Student's version, not Person's?
Why this lesson exists
Syllabus mapping
polymorphism- method overriding, abstract classes, the Object class.
Maps to course outcome CO2.