Skip to Content
Course content

30: Polymorphism in Practice

Click on the "Edit" button in the top corner of the screen to edit your slide content.

By now, you know the theory: polymorphism is just "many forms." But when you're actually staring at a blank IDE, it can feel like an academic exercise. You might be wondering, "Why go through the trouble of an inheritance hierarchy when I can just write a few if-statements?" Let's get into how this actually works in a professional codebase.

Why not just use a bunch of "if" statements to check the type?

I see this all the time with junior devs. You have a Payment class and three subclasses: CreditCard, PayPal, and Crypto. When it's time to process the payment, the instinct is to do something like this:

if (payment instanceof CreditCard) {
    ((CreditCard) payment).chargeCard();
} else if (payment instanceof PayPal) {
    ((PayPal) payment).redirecttoPayPal();
} // ... and so on

This is what I call "shotgun surgery." Every time your boss tells you to add a new payment method—say, Apple Pay—you have to hunt down every single one of these if/else blocks across your entire project and add a new case. If you miss one, the app crashes in production.

With polymorphism, you define a processPayment() method in the base Payment class. Each subclass overrides it. Now, your processing logic looks like this:

public void executeTransaction(Payment payment) {
    payment.processPayment(); // Java figures out which version to run at runtime
}

Now, when you add Apple Pay, you just create the new class. You don't have to touch the executeTransaction method at all. It just works. That's the real power here: your high-level logic becomes immune to changes in your low-level implementations.

If I'm treating everything as the base class, how do I access subclass-specific methods?

This is the part that trips people up. If you have a List<Payment>, you can only call methods defined in the Payment class. If CreditCard has a special method called validateCVV(), you can't just call it on a Payment object. The compiler will stop you because it can't guarantee that every payment is a credit card.

Sometimes, you actually do need that specific functionality. In those cases, you use downcasting. But do it safely. Use the instanceof operator (or the newer pattern matching in recent Java versions) to ensure you aren't forcing a Crypto payment to act like a CreditCard, which would throw a ClassCastException.

for (Payment p : payments) {
    p.processPayment(); // General action
    
    if (p instanceof CreditCard card) { 
        card.validateCVV(); // Specific action for cards only
    }
}

A word of advice: if you find yourself downcasting everywhere, your abstraction is probably leaking. You should probably move that "specific" logic into the overridden processPayment() method instead.

How do I actually design my classes to make this useful?

The secret is to program to an interface (or an abstract class), not an implementation. Stop thinking about "The Credit Card Class" and start thinking about "The Payment Behavior."

I usually start by creating an abstract class. I make it abstract because a generic "Payment" doesn't actually exist in the real world—you never just "pay" in a vacuum; you pay via something. By making it abstract, I force every developer who joins the project to implement the required methods in their specific subclasses.

public abstract class Payment {
    protected double amount;
    
    public Payment(double amount) { this.amount = amount; }
    
    // Every payment MUST implement this
    public abstract void processPayment(); 
    
    // Every payment shares this logic
    public void printReceipt() {
        System.out.println("Paid: $" + amount);
    }
}

By structuring it this way, you've created a contract. You're telling the rest of your system: "I don't care how the payment happens, as long as the object provides a processPayment() method." That's how you build software that can grow without breaking every five minutes.




📋 Practical Task

Exercise: Building a Multi-Channel Notification Dispatcher

You are building a system that sends alerts to users. Users can choose to receive alerts via different channels: Email, SMS, or Push Notification.

Your Goal: Implement a polymorphic system where a NotificationService can send a list of notifications without knowing which channel each one uses.

  • Create an abstract class Notification with a protected String message and an abstract method void send().
  • Create three subclasses: EmailNotification, SMSNotification, and PushNotification. Each should override send() to print a message like: "Sending Email: [message]".
  • Create a NotificationService class with a method dispatchAll(List<Notification> notifications). This method should loop through the list and call send() on each object.
  • In your main method, create a ArrayList of Notification objects containing at least one of each subclass, and pass that list to your NotificationService.

Challenge: Add a specific method to EmailNotification called setSubject(String subject). In your main method, use instanceof to check if a notification is an email, and if so, set a subject before sending it.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.