Polymorphism
Polymorphism means "many forms": one variable of a parent type can refer to objects of different subclasses, and each object responds in its own way.
class Shape {
double area() { return 0; }
String name() { return "shape"; }
}
class Circle extends Shape {
double r;
Circle(double r) { this.r = r; }
@Override double area() { return Math.PI * r * r; }
@Override String name() { return "circle"; }
}
class Rectangle extends Shape {
double w, h;
Rectangle(double w, double h) { this.w = w; this.h = h; }
@Override double area() { return w * h; }
@Override String name() { return "rectangle"; }
}
class Square extends Rectangle {
Square(double side) { super(side, side); }
@Override String name() { return "square"; }
}
public class Main {
public static void main(String[] args) {
Shape[] shapes = { new Circle(1), new Rectangle(3, 4), new Square(5) };
double total = 0;
for (Shape s : shapes) {
System.out.printf("%-9s area %.2f%n", s.name(), s.area());
total += s.area();
}
System.out.printf("total area %.2f%n", total);
}
}The loop only knows it holds Shapes. Which area() runs is decided while the program runs, by the actual object in each slot. This is called dynamic dispatch. Square doesn't even write its own area(): it inherits Rectangle's.
Why it matters
Without polymorphism, code that handles shapes fills up with checks:
if (s is a circle) { ... } else if (s is a rectangle) { ... } else if ...Every new kind of shape means finding and editing every such chain. With polymorphism, you add a Triangle class with its own area(), and every loop that handles Shapes works with it, unchanged.
Declared type and actual type
Shape s = new Circle(2);
s.area(); // fine: Shape declares area(), and Circle's version runs
s.r; // error: the declared type, Shape, has no field rThe declared type decides what you may call; the actual object decides which version runs.
instanceof and casting
class Animal {
String sound() { return "..."; }
}
class Dog extends Animal {
@Override String sound() { return "Woof"; }
void fetch() { System.out.println("Fetching the ball!"); }
}
class Cat extends Animal {
@Override String sound() { return "Meow"; }
}
public class Main {
public static void main(String[] args) {
Animal[] pets = { new Dog(), new Cat() };
for (Animal a : pets) {
System.out.println(a.sound());
if (a instanceof Dog) {
Dog d = (Dog) a; // safe: checked first
d.fetch();
}
}
Animal a = new Cat();
Dog wrong = (Dog) a; // ClassCastException: a Cat isn't a Dog
}
}instanceofchecks the actual type of an object.- A cast like
(Dog) alets you use Dog-only methods, and throws aClassCastExceptionif the object isn't really a Dog, as the last line shows. - Lots of
instanceofchecks are a hint that the behaviour belongs in an overridden method instead.
Since Java 16, if (a instanceof Dog d) checks and casts in one step. The code runner uses Java 15, so this lesson writes the cast out in full.
Overriding vs overloading
| Overriding | Overloading | |
|---|---|---|
| What | a subclass replaces a parent's method | several methods in one class share a name |
| Parameters | the same | different |
| Chosen | while running, by the actual object | when compiling, by the argument types |