Chapter[14]: Understanding Polymorphism in Java: A Real-World Perspective

Automate This.
5 min readJust now

--

Designed By Freepik

Polymorphism might sound like a complex term, but it’s actually a simple and powerful concept. It’s one of the cornerstones of Object-Oriented Programming (OOP). The term “polymorphism” comes from the Greek words “poly” (many) and “morph” (forms). It basically means that objects can take on many forms, making our code flexible, reusable, and scalable. Let’s dive into what it is with an easy example.

What is Polymorphism?

In Java, polymorphism allows the same action to behave differently depending on the object that’s calling it. Sounds fancy, right? It’s actually quite practical and happens in two main ways:

  1. Compile-Time Polymorphism (Method Overloading): This is when you define multiple methods with the same name but different parameter lists in the same class.
  2. Run-Time Polymorphism (Method Overriding): This is when a subclass provides its own specific implementation of a method that’s already defined in its parent class. Here, the actual method that’s called is determined at runtime.

Real-World Scenario: A Payment System

Let’s simplify this with a real-world example. Picture an e-commerce platform like Flipkart or Amazon. Customers can pay using various methods — credit cards, debit cards, or digital wallets. While the overall idea of “making a payment” stays the same, each method has its unique way of processing the transaction. And this is where polymorphism steps in to save the day!

1. Compile-Time Polymorphism (Method Overloading)

We need a way to handle payments where the input details might differ. For instance, some users may use a card while others might prefer a wallet.

class PaymentProcessor {
// Method to process payment with card details
void processPayment(String cardNumber, String expiryDate, double amount) {
System.out.println("Processing payment with card: " + cardNumber);
}

// Overloaded method to process payment with wallet
void processPayment(String walletId, double amount) {
System.out.println("Processing payment with digital wallet: " + walletId);
}

What’s Happening Here? Both methods are called processPayment, but their parameter lists are different. The right method is chosen at compile time based on the arguments we pass.

2. Run-Time Polymorphism (Method Overriding)

Now, let’s add some spice. Each payment method (like credit card or wallet) has its own unique way of processing payments. Using method overriding, we can customize the behavior for each payment type.

// Base class
class Payment {
void processPayment(double amount) {
System.out.println("Processing a generic payment of $" + amount);
}
}

// Subclass for Credit Card payment
class CreditCardPayment extends Payment {
@Override
void processPayment(double amount) {
System.out.println("Processing a credit card payment of $" + amount);
}
}

// Subclass for Wallet payment
class WalletPayment extends Payment {
@Override
void processPayment(double amount) {
System.out.println("Processing a wallet payment of $" + amount + " with cashback included!");
}
}

What’s Different Here?

  • The parent class Payment defines a general method processPayment.
  • The subclasses (CreditCardPayment and WalletPayment) override this method to provide their own unique behavior.

Putting Polymorphism to Work

Here’s how this all comes together in action:

public class ECommercePlatform {
public static void main(String[] args) {
Payment payment;

// Credit card payment
payment = new CreditCardPayment();
payment.processPayment(100.0);
// Wallet payment
payment = new WalletPayment();
payment.processPayment(50.0);
}
}

Output:

Processing a credit card payment of $100.0
Processing a wallet payment of $50.0 with cashback included!

Why Polymorphism Rocks

  1. Flexibility: You can add new payment methods (like UPI) without breaking or changing existing code.
  2. Reusability: The parent class’s methods can be reused, reducing duplication.
  3. Scalability: Your code can easily adapt to new requirements by overriding methods

Let’s dive into method overloading mistakes and possible solutions

The Problem

Say you’re building a CardPayment system just like the above that processes payments. You start with a method to handle payments for a single card like this:

public String processPayment(String cardNumber) {
return "Processing payment for card: " + cardNumber;
}

Now, you think, “What if I want another method to handle payments for a single card but return the payment amount as an int instead of a String?”

    public int processPayment(String digitalCardNumber) {
return 100; // Fixed payment amount
}

But boom! 🚨 Java throws a compilation error.

Why?

Java doesn’t allow overloading based solely on the return type, and different variable name, The method signature (processPayment(String)) is identical for both methods, so the compiler gets confused, even if their return types, and parameter names are different (String vs. int), and (cardNumber vs. digitalCardNumber).

The Fix

To properly overload the method, you need to vary the parameters, not just the return type. Here’s how you fix it:

class CardPayment {
// Method 1: Process payment with a single card
public String processPayment(String cardNumber) {
return "Processing payment for card: " + cardNumber;
}

// Method 2: Process payment with a card and amount
public String processPayment(String cardNumber, double amount) {
return "Processing payment of $" + amount + " for card: " + cardNumber;
}
// Method 3: Process payment with multiple cards
public String processPayment(String[] cardNumbers) {
return "Processing payment for " + cardNumbers.length + " cards.";
}
}

Using It

Here’s how you’d call these overloaded methods:

public class Main {
public static void main(String[] args) {
CardPayment paymentProcessor = new CardPayment();

// Single card payment
System.out.println(paymentProcessor.processPayment("1234-5678-9012-3456"));
// Single card with an amount
System.out.println(paymentProcessor.processPayment("1234-5678-9012-3456", 150.75));
// Multiple card payments
String[] cards = { "1234-5678-9012-3456", "9876-5432-1098-7654" };
System.out.println(paymentProcessor.processPayment(cards));
}
}

The Output

Processing payment for card: 1234-5678-9012-3456
Processing payment of $150.75 for card: 1234-5678-9012-3456
Processing payment for 2 cards.

Key Takeaways

1: Rule: Overloading requires different parameters, not just different return types.

  • For example, these two won’t work:
public String processPayment(String cardNumber); 
public int processPayment(String cardNumber);

2: Proper Overloading: Change the number or type of parameters. Examples:

  • Add a second parameter (String cardNumber, double amount).
  • Use a different type (String[] cardNumbers).

3: Be Mindful of Ambiguity:

  • If overloading with similar parameter types (e.g., String and Object), ensure the calls are unambiguous to avoid confusion.

Polymorphism makes your code cleaner and smarter! In our payment system, it helped handle different payment types without a fuss. Big apps like ride-sharing or banking use the same concept to manage tons of actions efficiently. Mastering it means you’ll write code that’s not just clean but also ready to grow and adapt. Got any questions or want another example? Let’s keep the conversation going!

Find and Download the code here

.

.

.

Happy Coding!

--

--

Automate This.
Automate This.

Written by Automate This.

Join me as I share insights, tips, and experiences from my journey in quality assurance, automation, and coding! https://www.linkedin.com/in/iammriganksaxena/

No responses yet