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));
    }
}
Output:
2 * 3 = 6
2 * 3 * 4 = 24
2.5 * 3.2 = 8.0

Explanation:

  • All three methods are named multiply but 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()
    }
}
Output:
Dog barks: Woof! Woof!
Cat meows: Meow! Meow!
Cow moos: Moo! Moo!

Explanation:

  • Parent reference myAnimal points 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();
    }
}
Output:
Savings Account Interest (4%): $400.0
Current Account Interest (1%): $100.0
Fixed Deposit Interest (7%): $700.0

Explanation:

  • One parent reference account works 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
        }
    }
}
Output:
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 instanceof operator 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);
    }
}
Output:
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 Payment reference 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!