🎭 Polymorphism
Understand compile-time and runtime polymorphism, method overloading, overriding, and dynamic dispatch.
What is Polymorphism?
Polymorphism comes from Greek words meaning "many forms". In Java, it allows us to perform a single action in different ways. It is a core concept of OOP that lets you use the same interface for different underlying forms (data types).
Types of Polymorphism
- Compile-time Polymorphism: Achieved through Method Overloading.
- Runtime Polymorphism: Achieved through Method Overriding (Dynamic Method Dispatch).
Compile-time Polymorphism (Method Overloading)
Method overloading allows multiple methods to have the same name but different parameters (number, type, or order of arguments). The compiler determines which method to execute based on the method signature during compilation.
class MathOperations { // Adds two integers public int add(int a, int b) { return a + b; } // Adds three integers public int add(int a, int b, int c) { return a + b + c; } // Adds two doubles public double add(double a, double b) { return a + b; } } public class MethodOverloadingDemo { public static void main(String[] args) { MathOperations math = new MathOperations(); System.out.println(math.add(10, 20)); // Calls add(int, int) System.out.println(math.add(10, 20, 30)); // Calls add(int, int, int) System.out.println(math.add(5.5, 2.5)); // Calls add(double, double) } }
Runtime Polymorphism (Method Overriding)
This is a process in which a call to an overridden method is resolved at runtime rather than compile-time. This is known as Dynamic Method Dispatch. It relies on a superclass reference variable holding a subclass object (upcasting).
class Animal { void makeSound() { System.out.println("Some generic animal sound"); } } class Cat extends Animal { @Override void makeSound() { System.out.println("Meow"); } } class Dog extends Animal { @Override void makeSound() { System.out.println("Woof"); } } class Bird extends Animal { @Override void makeSound() { System.out.println("Chirp"); } } public class AnimalSound { public static void main(String[] args) { Animal a; // Superclass reference a = new Cat(); // Upcasting a.makeSound(); // Output: Meow a = new Dog(); a.makeSound(); // Output: Woof a = new Bird(); a.makeSound(); // Output: Chirp } }
Polymorphic Arrays
Because a superclass reference can point to subclass objects, you can create arrays of a superclass type and store any subclass object inside them. This makes processing lists of different but related objects very clean.
class Shape { void draw() { System.out.println("Drawing shape"); } } class Circle extends Shape { void draw() { System.out.println("Drawing circle"); } } class Rectangle extends Shape { void draw() { System.out.println("Drawing rectangle"); } } public class PolymorphicArray { public static void main(String[] args) { Shape[] shapes = new Shape[3]; shapes[0] = new Shape(); shapes[1] = new Circle(); shapes[2] = new Rectangle(); for (Shape s : shapes) { s.draw(); // Polymorphic call } } }
Upcasting, Downcasting and instanceof
Upcasting is converting a subclass reference into a superclass reference (done implicitly). Downcasting is converting a superclass reference back into a subclass reference (requires explicit cast).
The instanceof operator checks whether an object is an instance of a specific class, preventing ClassCastException during downcasting.
class Vehicle {} class Car extends Vehicle { void honk() { System.out.println("Beep beep!"); } } public class DowncastingDemo { public static void main(String[] args) { Vehicle v = new Car(); // Upcasting // v.honk(); // ERROR: Vehicle does not have honk() if (v instanceof Car) { Car c = (Car) v; // Downcasting c.honk(); } } }