Java Polymorphism
Unlock the power of "one interface, multiple implementations" – where objects can take many forms and behave differently based on context.
What is Polymorphism?
Polymorphism is one of the four pillars of Object-Oriented Programming (OOP). The word "polymorphism" comes from Greek, meaning "many forms". In Java, it means that a single action can be performed in different ways.
Think of a real-world example: A person can be a student, an employee, a parent, or a friend at the same time. The same person behaves differently in different situations – that's polymorphism in real life!
- One Interface, Multiple Implementations: Same method name, different behaviors
- Flexibility: Write code that works with parent class but executes child class behavior
- Maintainability: Add new classes without changing existing code
Simple Analogy: Imagine a smartphone's "Press Button" action. Pressing the power button turns the phone on/off, pressing volume buttons changes sound, pressing home button goes to home screen. Same action (press), different results based on which button – that's polymorphism!
Types of Polymorphism in Java
1. Compile-time Polymorphism (Static Polymorphism)
Also known as Method Overloading, this type is resolved during compilation. Multiple methods with the same name but different parameters exist in the same class.
Examples: add(int, int), add(int, int, int), add(double, double) – All methods named "add" but with different parameters
2. Runtime Polymorphism (Dynamic Polymorphism)
Also known as Method Overriding, this type is resolved during runtime. A child class provides its own implementation of a method already defined in the parent class.
Examples: Animal class has eat() method, Dog class overrides it with its own eat() behavior, Cat class overrides with different eat() behavior
Compile-time Polymorphism Example
Method overloading allows us to create multiple methods with the same name but different parameters, decided at compile time.
Example: Method Overloading (Compile-time)
class MathOperations {
// Method with 2 int parameters
int multiply(int a, int b) {
return a * b;
}
// Method with 3 int parameters
int multiply(int a, int b, int c) {
return a * b * c;
}
// Method with 2 double parameters
double multiply(double a, double b) {
return a * b;
}
}
public class Main {
public static void main(String[] args) {
MathOperations math = new MathOperations();
System.out.println("2 * 3 = " + math.multiply(2, 3));
System.out.println("2 * 3 * 4 = " + math.multiply(2, 3, 4));
System.out.println("2.5 * 3.2 = " + math.multiply(2.5, 3.2));
}
}
2 * 3 = 6 2 * 3 * 4 = 24 2.5 * 3.2 = 8.0
Explanation:
- All three methods are named
multiplybut have different parameters - Java decides which method to call at compile time based on arguments
- This is compile-time polymorphism because the decision is made during compilation
- Same method name performs different operations based on input
Runtime Polymorphism Example
Method overriding allows child classes to provide their own implementation of parent class methods, decided at runtime.
Example: Method Overriding (Runtime)
class Animal {
void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Dog barks: Woof! Woof!");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("Cat meows: Meow! Meow!");
}
}
class Cow extends Animal {
@Override
void sound() {
System.out.println("Cow moos: Moo! Moo!");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal; // Parent reference variable
myAnimal = new Dog();
myAnimal.sound(); // Calls Dog's sound()
myAnimal = new Cat();
myAnimal.sound(); // Calls Cat's sound()
myAnimal = new Cow();
myAnimal.sound(); // Calls Cow's sound()
}
}
Dog barks: Woof! Woof! Cat meows: Meow! Meow! Cow moos: Moo! Moo!
Explanation:
- Parent reference
myAnimalpoints to different child objects (Dog, Cat, Cow) - Each child class overrides the
sound()method with its own implementation - The method called is determined at runtime based on the actual object type
- This is runtime polymorphism – the JVM decides which method to call during execution
Practical Runtime Polymorphism Example
A banking system where different account types have different interest calculation methods – perfect for polymorphism!
Example: Bank Account with Polymorphism
class BankAccount {
double balance;
BankAccount(double balance) {
this.balance = balance;
}
void calculateInterest() {
System.out.println("Standard interest calculation");
}
}
class SavingsAccount extends BankAccount {
SavingsAccount(double balance) {
super(balance);
}
@Override
void calculateInterest() {
double interest = balance * 0.04; // 4% interest
System.out.println("Savings Account Interest (4%): $" + interest);
}
}
class CurrentAccount extends BankAccount {
CurrentAccount(double balance) {
super(balance);
}
@Override
void calculateInterest() {
double interest = balance * 0.01; // 1% interest
System.out.println("Current Account Interest (1%): $" + interest);
}
}
class FixedDeposit extends BankAccount {
FixedDeposit(double balance) {
super(balance);
}
@Override
void calculateInterest() {
double interest = balance * 0.07; // 7% interest
System.out.println("Fixed Deposit Interest (7%): $" + interest);
}
}
public class Main {
public static void main(String[] args) {
BankAccount account;
account = new SavingsAccount(10000);
account.calculateInterest();
account = new CurrentAccount(10000);
account.calculateInterest();
account = new FixedDeposit(10000);
account.calculateInterest();
}
}
Savings Account Interest (4%): $400.0 Current Account Interest (1%): $100.0 Fixed Deposit Interest (7%): $700.0
Explanation:
- One parent reference
accountworks with all account types - Each account type calculates interest differently
- The correct interest method is called based on the actual object at runtime
- This demonstrates real-world use: one interface, multiple implementations
Upcasting and Downcasting
Understanding how parent and child references work is crucial for mastering polymorphism.
Example: Upcasting and Downcasting
class Vehicle {
void run() {
System.out.println("Vehicle is running");
}
}
class Car extends Vehicle {
@Override
void run() {
System.out.println("Car is running");
}
void playMusic() {
System.out.println("Car is playing music");
}
}
public class Main {
public static void main(String[] args) {
// Upcasting (implicit) - Child to Parent
Vehicle v = new Car(); // Car object, Vehicle reference
v.run(); // Calls Car's run() method (polymorphism)
// v.playMusic(); // Error! Vehicle reference can't see Car-specific methods
// Downcasting (explicit) - Parent to Child
if (v instanceof Car) {
Car c = (Car) v; // Cast Vehicle reference to Car
c.playMusic(); // Now we can call Car-specific methods
}
}
}
Car is running Car is playing music
Explanation:
- Upcasting: Child object referenced by parent type – happens automatically
- Downcasting: Parent reference cast back to child type – must be explicit
- Parent reference can only access methods defined in parent class
- Use
instanceofoperator to check object type before downcasting
Key Differences: Compile-time vs Runtime
Comparison Table
- Compile-time Polymorphism: Achieved through method overloading, resolved at compile time, same class, multiple methods with same name but different parameters
- Runtime Polymorphism: Achieved through method overriding, resolved at runtime, parent-child relationship, same method signature with different implementations
- Performance: Compile-time is faster (decision made during compilation), Runtime is slightly slower (decision made during execution)
- Flexibility: Compile-time is less flexible, Runtime provides more flexibility and extensibility
Remember: Overloading = Compile-time = Same class, different parameters. Overriding = Runtime = Parent-child, same signature, different implementation.
Real-World Polymorphism Example
A payment system where different payment methods process transactions differently – showcasing polymorphism's power!
Example: Payment Processing System
class Payment {
void processPayment(double amount) {
System.out.println("Processing payment of $" + amount);
}
}
class CreditCard extends Payment {
@Override
void processPayment(double amount) {
System.out.println("Processing Credit Card payment of $" + amount);
System.out.println("Transaction fee: $" + (amount * 0.02));
}
}
class DebitCard extends Payment {
@Override
void processPayment(double amount) {
System.out.println("Processing Debit Card payment of $" + amount);
System.out.println("Transaction fee: $" + (amount * 0.01));
}
}
class PayPal extends Payment {
@Override
void processPayment(double amount) {
System.out.println("Processing PayPal payment of $" + amount);
System.out.println("Transaction fee: $" + (amount * 0.03));
}
}
class Bitcoin extends Payment {
@Override
void processPayment(double amount) {
System.out.println("Processing Bitcoin payment of $" + amount);
System.out.println("No transaction fee!");
}
}
public class Main {
public static void main(String[] args) {
Payment payment;
payment = new CreditCard();
payment.processPayment(1000);
System.out.println();
payment = new DebitCard();
payment.processPayment(1000);
System.out.println();
payment = new PayPal();
payment.processPayment(1000);
System.out.println();
payment = new Bitcoin();
payment.processPayment(1000);
}
}
Processing Credit Card payment of $1000.0 Transaction fee: $20.0 Processing Debit Card payment of $1000.0 Transaction fee: $10.0 Processing PayPal payment of $1000.0 Transaction fee: $30.0 Processing Bitcoin payment of $1000.0 No transaction fee!
Explanation:
- Single
Paymentreference handles all payment types - Each payment method has its own processing logic and fee structure
- Adding new payment methods doesn't require changing existing code
- This demonstrates polymorphism's real-world power: extensibility and maintainability
Benefits of Polymorphism
- Code Reusability: Write general code that works with parent class, use with any child class
- Flexibility: Easily add new classes without modifying existing code
- Maintainability: Changes in child classes don't affect parent class or other children
- Loose Coupling: Components depend on interfaces/abstractions, not concrete implementations
- Cleaner Code: One interface for multiple implementations reduces complexity
- Extensibility: System can grow without breaking existing functionality
Design Principle: "Program to an interface, not an implementation" – This is the heart of polymorphism. Your code should work with parent types, allowing any child implementation to work seamlessly!
Important Rules for Polymorphism
- IS-A Relationship: Runtime polymorphism requires inheritance (child IS-A parent)
- Method Signature: Overriding requires exact same method signature in parent and child
- Access Modifiers: Child method can't have more restrictive access than parent
- Final Methods: Methods declared as final cannot be overridden
- Static Methods: Static methods cannot be overridden (but can be hidden)
- Private Methods: Private methods in parent are not inherited, so can't be overridden
Pro Tip: Use @Override annotation when overriding methods. It's not mandatory, but it helps catch errors at compile time if you accidentally create a new method instead of overriding!
Key Points to Remember
- Polymorphism means "many forms" – same interface, different implementations
- Two types: Compile-time (overloading) and Runtime (overriding)
- Runtime polymorphism is achieved through method overriding and inheritance
- Parent reference can hold child objects, but only access parent-defined methods
- The actual method called is determined by the object type, not reference type
- Polymorphism is essential for building flexible, maintainable, and extensible applications
Final Thought: Polymorphism is like a universal remote control. One remote (parent reference) can control different devices (child objects) – TV, AC, music system. Each device responds differently to the same button press. That's the beauty of polymorphism!