Polymorphism in Java
Polymorphism means "many forms." In Java, it allows objects to be treated as instances of their parent class or interface, while the actual behavior is determined by their real type. It's one of the four pillars of OOP.
What is Polymorphism?
Polymorphism allows you to write code that works with objects of different types through a common interface. The same method call can behave differently depending on the actual object type.
Animal myAnimal = new Dog(); // Polymorphism in action!
myAnimal.makeSound(); // Calls Dog's makeSound(), not Animal's
myAnimal = new Cat(); // Same variable, different object
myAnimal.makeSound(); // Now calls Cat's makeSound()
- Compile-time (Static): Method overloading - decided at compile time
- Runtime (Dynamic): Method overriding - decided at runtime
Runtime Polymorphism (Dynamic Method Dispatch)
When you call an overridden method through a parent class reference, Java determines which version to call at runtime based on the actual object type.
class Animal {
void makeSound() {
System.out.println("Some animal sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Meow!");
}
}
// Runtime polymorphism
Animal animal1 = new Dog(); // Reference: Animal, Object: Dog
Animal animal2 = new Cat(); // Reference: Animal, Object: Cat
animal1.makeSound(); // Output: "Woof!" (Dog's method)
animal2.makeSound(); // Output: "Meow!" (Cat's method)
Compile-time Polymorphism (Method Overloading)
Method overloading allows multiple methods with the same name but different parameters. The compiler decides which method to call based on the arguments.
class Calculator {
// Same method name, different parameters
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
}
Calculator calc = new Calculator();
calc.add(5, 3); // Calls add(int, int)
calc.add(5.0, 3.0); // Calls add(double, double)
calc.add(1, 2, 3); // Calls add(int, int, int)
Upcasting and Downcasting
Upcasting is casting to a parent type (implicit, always safe). Downcasting is casting to a child type (explicit, can fail).
// Upcasting (implicit, safe)
Dog dog = new Dog();
Animal animal = dog; // Dog -> Animal (automatic)
// Downcasting (explicit, needs instanceof check)
Animal animal2 = new Dog();
if (animal2 instanceof Dog) {
Dog dog2 = (Dog) animal2; // Animal -> Dog (explicit cast)
dog2.fetch(); // Can now call Dog-specific methods
}
Animal animal = new Cat();
Dog dog = (Dog) animal; // ClassCastException! Cat is not a Dog
Polymorphic Arrays and Collections
Arrays and collections of parent type can hold objects of any subclass:
// Array of parent type
Animal[] zoo = new Animal[3];
zoo[0] = new Dog();
zoo[1] = new Cat();
zoo[2] = new Bird();
// Process all animals uniformly
for (Animal animal : zoo) {
animal.makeSound(); // Each calls its own version
}
Click Run to execute your code
Interface Polymorphism
Polymorphism works with interfaces too - different classes can implement the same interface differently:
interface Drawable {
void draw();
}
class Circle implements Drawable {
public void draw() { System.out.println("Drawing Circle"); }
}
class Rectangle implements Drawable {
public void draw() { System.out.println("Drawing Rectangle"); }
}
// Interface polymorphism
Drawable[] shapes = {new Circle(), new Rectangle()};
for (Drawable shape : shapes) {
shape.draw(); // Polymorphic call
}
Benefits of Polymorphism
- Code Reusability: Write code that works with parent class/interface, reuse with all subclasses
- Flexibility: Add new classes without changing existing code
- Maintainability: Changes in one class don't affect others
- Extensibility: System is open for extension, closed for modification (Open/Closed Principle)
Real-World Example: Payment Processing
abstract class Payment {
abstract void processPayment(double amount);
}
class CreditCardPayment extends Payment {
@Override
void processPayment(double amount) {
System.out.println("Processing $" + amount + " via Credit Card");
}
}
class PayPalPayment extends Payment {
@Override
void processPayment(double amount) {
System.out.println("Processing $" + amount + " via PayPal");
}
}
class BitcoinPayment extends Payment {
@Override
void processPayment(double amount) {
System.out.println("Processing $" + amount + " via Bitcoin");
}
}
// Polymorphic processing - works with ANY payment type
void checkout(Payment payment, double amount) {
payment.processPayment(amount);
}
// Usage
checkout(new CreditCardPayment(), 100.00);
checkout(new PayPalPayment(), 50.00);
checkout(new BitcoinPayment(), 200.00);
Common Mistakes
- Confusing overriding with overloading: Overriding is runtime polymorphism; overloading is compile-time
- Downcasting without instanceof: Always check type before downcasting
- Trying to call child-specific methods on parent reference: The reference type determines available methods (at compile time)
- Forgetting @Override annotation: Use it to catch errors in method signatures
Summary
- Polymorphism means "many forms" - same interface, different behaviors
- Runtime polymorphism: Method overriding - determined at runtime
- Compile-time polymorphism: Method overloading - determined at compile time
- Upcasting is implicit and safe; downcasting requires explicit cast and type check
- Use polymorphic arrays/collections to process different object types uniformly
- Polymorphism enables flexible, extensible, and maintainable code
Enjoying these tutorials?