Understanding Polymorphism in Object-Oriented Programming
Polymorphism is the ability of a single interface to represent different underlying forms (data types or classes). It enables objects of various subclasses to be treated uniformly through a common superclass reference.
2. How Does Polymorphism Manifest?
It occurs when a reference variable of a parent class type holds an instance of a subclass:
Animal pet = new Dog(); // Valid: upcasting via assignment
This assignment relies on upcasting — an implicit, safe conversion from a more specific (subclass) to a more general (superclass) type.
3. Prerequisites for Polymorphism
- Existence of an inheritance or interface implementation hierarchy
- A superclass (or interface) reference pointing to a subclass (or implementing) object
- Method overirding (required for dynamic dispatch behavior)
4. Key Advantages
- Code reusability & extensibility: Methods accepting superclass parameters can operate on any compatbile subclass instance without modification.
- Runtime flexibility: When overridden methods are invoked, the JVM selects the implementation based on the actual runtime object type, not the declared reference type.
5. Member Access Behavior Under Polymorphism
| Member Type | Compile-Time Resolution | Run-Time Resolution | Supports Polymorphism? |
|---|---|---|---|
| Instance Variables | Reference type (left side) | Reference type (left side) | No — binding is static and fixed at compile time |
| Instance Methods | Reference type (must declare method signature) | Actual object type (right side) | Yes — resolved dynamically via virtual method table |
Illustration: Variable Hiding vs. Method Overriding
class Vehicle {
String brand = "Generic";
}
class ElectricCar extends Vehicle {
String brand = "Tesla"; // hides Vehicle.brand — not overridden
}
Vehicle v = new ElectricCar();
System.out.println(v.brand); // Output: "Generic"
Here, brand is hidden, not overridden. Field access depends solely on the declared type (Vehicle), regardless of the instantiated object.
In contrast, method calls obey dynamic dispatch:
class Vehicle {
void start() { System.out.println("Engine starting..."); }
}
class ElectricCar extends Vehicle {
@Override
void start() { System.out.println("Motor whirring..."); }
}
Vehicle v = new ElectricCar();
v.start(); // Output: "Motor whirring..."
6. Compile-Time Safety Constraints
The compiler validates only against the declared reference type. If ElectricCar defines a chargeBattery() method absent in Vehicle, this code fails compilation:
v.chargeBattery(); // ❌ Compilation error: cannot resolve symbol
This enforces abstraction and prevents accidental misuse of subclass-specific contracts.
7. Limitation of Polymorphic References
A polymorphic reference cannot directly invoke subclass-specific members unless explicitly cast. This restricts access to features beyond the shared contract defined by the superclass or interface.
8. Type Conversion Strategies
- Upcasting: Implicit and automatic (e.g.,
Animal a = new Dog();) - Downcasting: Explicit and requires safety checks (e.g.,
Dog d = (Dog) a;)
9. Safe Downcasting with instanceof
To avoid ClassCastException, verify compatibility before casting:
if (pet instanceof Dog) {
Dog dog = (Dog) pet;
dog.bark(); // Now safe to call Dog-specific behavior
}
This pattern supports type-aware logic branching:
void administerCare(Animal creature) {
if (creature instanceof Dog) {
System.out.println("Giving dog treats and leash walk.");
} else if (creature instanceof Bird) {
System.out.println("Providing perches and seed mix.");
}
}
Such conditional handling leverages runtime type information while preserving compile-time safety.