Polymorphism: Many Forms, One Interface
Polymorphism comes from the Greek words Poly (many) and Morph (form). In Java, it refers to the ability of an object to take on many forms. Most commonly, it allows us to treat a child object as if it were an instance of its parent class.
Think of a Smartphone. It can act as a Phone, a Camera, a Calculator, or a GPS. It is one device that can take on many "forms" depending on which app you open.
1. Two Types of Polymorphism
A. Static (Compile-time) Polymorphism
This is achieved through Method Overloading. It happens when multiple methods in the same class have the same name but different parameters. Java decides which one to run based on the arguments you provide while you are writing the code.
class Printer {
void print(String text) { System.out.println(text); }
void print(int number) { System.out.println("Num: " + number); }
}
B. Dynamic (Runtime) Polymorphism
This is achieved through Method Overriding. It happens when a subclass provides its own version of a parent's method. Java decides which version to run at runtime based on the actual object type, not the variable type.
Animal myPet = new Dog();
myPet.makeSound(); // Even though the variable is 'Animal', it runs the Dog's bark!
2. Upcasting: The Secret Sauce
Upcasting is when you store a child object in a parent variable. This is what allows you to write extremely flexible code.
// We treat different specific things as one general thing
Vehicle v1 = new Car();
Vehicle v2 = new Motorcycle();
Vehicle v3 = new Truck();
// We can put them all in one list!
Vehicle[] garage = {v1, v2, v3};
for (Vehicle v : garage) {
v.startEngine(); // Each one starts in its own specific way
}
💻 Full Practical Example: The Shape Renderer
Copy this code to see how polymorphism allows you to handle a list of different objects with a single loop:
class Shape {
void draw() {
System.out.println("Drawing a generic shape...");
}
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("Drawing a Circle â—¯");
}
}
class Square extends Shape {
@Override
void draw() {
System.out.println("Drawing a Square â–¡");
}
}
public class Main {
public static void main(String[] args) {
// Polymorphic Array: Different shapes, one array type
Shape[] shapes = {new Circle(), new Square(), new Shape()};
for (Shape s : shapes) {
s.draw(); // Java automatically finds the right 'draw' for each object
}
}
}
3. Why is this useful?
- Extensibility: You can add a
Triangleclass later, and yourMaincode (the loop) won't have to change at all. It will just work! - Cleaner Code: You don't need complex
if-elseorswitchstatements to check what kind of object you are dealing with.
💡 Challenge: The Game Manager
- Create a class
Enemywith a methodtakeDamage(). - Create subclasses
ZombieandBoss. - Override
takeDamage()so theZombiedies instantly, but theBosssays "I have 500 HP left!". - In
main, create anArrayListor Array ofEnemyobjects, add one of each, and loop through them callingtakeDamage().