Back to Polymorphism
Q4Question 4 of 7

What is polymorphism and why is it useful?

Key Concepts
  • Polymorphism means one call, many behaviors depending on the actual object type
  • Compile-time polymorphism (overloading): compiler picks by argument types (static dispatch)
  • Runtime polymorphism (overriding): JVM dispatches to the actual object's method (dynamic dispatch via vtable)
  • Eliminates growing if-else and switch chains by letting each type handle its own behavior
  • Enables the Open/Closed Principle: add new types without modifying existing code
  • Powers the Strategy pattern: swap behavior at runtime by injecting different implementations

Definition

Polymorphism means one call, many behaviors. Compile-time: overloading (compiler picks by args). Runtime: overriding (JVM picks by actual type). It eliminates if-else chains, enables the Open/Closed Principle, and powers the Strategy pattern.

Polymorphism means many forms: one interface, multiple implementations.

  • Compile-time polymorphism (method overloading): same name, different parameters. The compiler picks the matching signature based on argument types at compile time.
  • Runtime polymorphism (method overriding): same signature in parent and child. The JVM picks the correct version based on the actual object type at runtime via the vtable.
  • Benefit 1: eliminates growing if-else and switch chains by letting each type handle its own behavior.
  • Benefit 2: enables the Open/Closed Principle, where you add new types without modifying existing code.
  • Benefit 3: powers the Strategy pattern, where you swap behavior at runtime by injecting different implementations.
  • Benefit 4: enables generic processing, where one loop handles all types polymorphically.

The bank's notification system needs to send messages: SMS for savings customers, email for premium customers, push for mobile customers. Without polymorphism: if (type == "SMS") sendSms(); else if (type == "EMAIL") sendEmail(); else if (type == "PUSH") sendPush(); and the chain grows with every new channel. With polymorphism: notificationChannel.send(message). One call. The JVM picks the right implementation at runtime. Adding WhatsApp: create WhatsAppChannel implements NotificationChannel. Zero changes to existing code.

Understanding the Concepts

What is polymorphism and why is it useful?

The real power of polymorphism is in runtime polymorphism. It enables designing systems that are extensible without modification. Compile-time polymorphism (overloading) is convenient but does not enable extensibility.

How does polymorphism eliminate if-else chains?

Without polymorphism, each new type requires a new branch in every switch or if-else chain.

JAVA
// WRONG: growing if-else chain
public java.math.BigDecimal calculateFee(String accountType, java.math.BigDecimal amount) {
    if ("SAVINGS".equals(accountType)) {
        return amount.multiply(new java.math.BigDecimal("0.001"));
    } else if ("CURRENT".equals(accountType)) {
        return amount.multiply(new java.math.BigDecimal("0.002"));
    } else if ("NRI".equals(accountType)) {
        return amount.multiply(new java.math.BigDecimal("0.003"));
    }
    // Add 10 more types -> 10 more else-if branches.
    throw new IllegalArgumentException("Unknown type: " + accountType);
}

// CORRECT: each type handles its own fee
public interface FeePolicy {
    java.math.BigDecimal calculateFee(java.math.BigDecimal amount);
}
public class SavingsFeePolicy implements FeePolicy {
    @Override public java.math.BigDecimal calculateFee(java.math.BigDecimal amount) {
        return amount.multiply(new java.math.BigDecimal("0.001"));
    }
}
public class PremiumFeePolicy implements FeePolicy {
    @Override public java.math.BigDecimal calculateFee(java.math.BigDecimal amount) {
        return java.math.BigDecimal.ZERO;
    }
}
// Usage: one line, no if-else.
java.math.BigDecimal fee = account.getFeePolicy().calculateFee(amount);

How does polymorphism enable the Open/Closed Principle?

The Open/Closed Principle states that code should be open for extension but closed for modification. Polymorphism achieves this: add new types without changing existing code.

JAVA
public class PaymentProcessor {
    public void process(PaymentChannel channel, java.math.BigDecimal amount) {
        channel.debit(amount);        // polymorphic call
        channel.generateReceipt();    // polymorphic call
        channel.notifyCustomer();     // polymorphic call
    }
}
// Today: NeftChannel, UpiChannel, RtgsChannel
// Tomorrow: add CbdcChannel (digital rupee) -> zero changes to PaymentProcessor.

How does polymorphism power the Strategy pattern?

The Strategy pattern swaps behavior at runtime by injecting different implementations.

JAVA
public class AccountService {
    private final InterestStrategy interestStrategy; // injected

    public AccountService(InterestStrategy strategy) {
        this.interestStrategy = strategy;
    }

    public java.math.BigDecimal calculateInterest(Account account) {
        return interestStrategy.calculate(account);
        // Could be SimpleInterest, CompoundInterest, or TieredInterest.
        // Swap strategy via configuration. Zero code change.
    }
}

How does polymorphism enable generic processing?

One loop processes all account types without type checks.

JAVA
public void processMonthEnd(java.util.List<Account> allAccounts) {
    for (Account account : allAccounts) {
        java.math.BigDecimal interest = account.calculateInterest();  // polymorphic
        java.math.BigDecimal fee = account.calculateFee();             // polymorphic
        account.applyMonthEnd(interest, fee);                          // polymorphic
    }
    // SavingsAccount: 4% interest, 0.1% fee
    // CurrentAccount: 0% interest, 0.2% fee
    // NRIAccount: 3.5% interest, 0.3% fee, FEMA check
    // One loop. Zero type checks. Fully extensible.
}

How is compile-time polymorphism different from runtime?

| Aspect | Compile-time (overloading) | Runtime (overriding) |
|--------|--------------------------|---------------------|
| Mechanism | Different parameter lists | Same signature, child replaces parent |
| Resolved by | Compiler (argument types) | JVM (actual object type, vtable) |
| Extensibility | No: adding new form requires modifying the class | Yes: new subclass, zero changes to existing |
| Dispatch | Static | Dynamic |
| Use case | Convenience (multiple deposit forms) | Flexibility (new account types) |

What are common mistakes with polymorphism?

The most common mistake is using instanceof checks instead of letting polymorphism do the work.

JAVA
// WRONG: checking type and branching
if (account instanceof SavingsAccount) {
    // savings logic
} else if (account instanceof CurrentAccount) {
    // current logic
}
// Adding NRIAccount: must add another branch. Breaks Open/Closed.

// CORRECT: let each type handle its own behavior
account.processMonthEnd(); // SavingsAccount, CurrentAccount, NRIAccount each override

What should a senior engineer know?

  • The practical test of polymorphism: can you add a new type without modifying existing code? If your design requires modifying a switch statement to add a new type, you are not using polymorphism correctly.
  • In Spring, polymorphism is used everywhere: multiple implementations of an interface are registered as beans, and the correct one is injected via @Qualifier or @Primary.
  • Sealed classes (Java 17) combine polymorphism with exhaustive pattern matching: the compiler ensures all subtypes are handled in a switch, and adding a new subtype causes compile errors everywhere it is not handled.

Visual Overview

DIAGRAM
  WITHOUT POLYMORPHISM
  ┌─────────────────────────┐
  │  if (type == SAVINGS)    │
  │    calculateSavingsFee() │
  │  else if (type == CURRENT)│
  │    calculateCurrentFee() │
  │  else if (type == NRI)   │
  │    calculateNriFee()     │
  │  else if ...             │  <- grows with every new type
  └─────────────────────────┘

  WITH POLYMORPHISM
  ┌─────────────────────────┐
  │  account.calculateFee()  │  <- one line
  │         │                │
  │         ▼                │
  │  JVM dispatches to:      │
  │  ├─ SavingsAccount: 0.1% │
  │  ├─ CurrentAccount: 0.2% │
  │  ├─ NRIAccount: 0.3%     │
  │  └─ FDAccount: 0%        │  <- new type, zero changes
  └─────────────────────────┘

Key Takeaway

Polymorphism means many forms: one interface, multiple implementations. Compile-time polymorphism is method overloading: same name, different parameters, compiler picks. Runtime polymorphism is method overriding: same signature, different classes, JVM picks based on actual object type. Runtime polymorphism is the more powerful form because it enables four critical production benefits: eliminating growing if-else chains, Open/Closed Principle (add types without modifying existing code), Strategy pattern (swap behavior at runtime via injection), and generic processing (one loop handles all types).

In Pankaj's notification dispatch service at National Bank of India, channel.send(recipient, message) dispatches to SmsChannel, EmailChannel, PushChannel, or WhatsAppChannel at runtime. One line replaces a 200-line switch statement. When WhatsApp was added: one new class implementing the interface, one bean registration. NotificationDispatcher: zero lines changed. Existing channels: zero lines changed. Existing tests: still pass.

Remember one sentence: polymorphism eliminates conditional logic and enables extension without modification, which is the foundation of maintainable software.

---

Share
Use ← → arrow keys to navigate