Skip to Content
Course content

31: Interfaces and Default Methods

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

I want to show you something that happens all the time in professional projects: the "API breaking change." It's that moment where you realize you need to add a new capability to a system, but you've already written twenty different classes that implement a specific interface, and changing that interface would force you to touch every single one of those files.

Setting up a simple contract

Let's imagine we're building a payment system. I'll start with a basic PaymentProcessor interface. At this stage, it's straightforward—every processor just needs to be able to handle a payment.

public interface PaymentProcessor {
    void processPayment(double amount);
}

Now, I'll write two implementations: one for Credit Cards and one for PayPal. Nothing fancy here.

public class CreditCardProcessor implements PaymentProcessor {
    public void processPayment(double amount) {
        System.out.println("Processing credit card payment of $" + amount);
    }
}

public class PayPalProcessor implements PaymentProcessor {
    public void processPayment(double amount) {
        System.out.println("Processing PayPal payment of $" + amount);
    }
}

This works perfectly. I can treat both of these as PaymentProcessor objects and call processPayment without caring which one is which. This is the core beauty of interfaces.

Breaking everything by accident

Here is where the trouble starts. My boss comes to me and says, "We need to support refunds." Naturally, I think, "Easy, I'll just add a refundPayment method to the interface."

public interface PaymentProcessor {
    void processPayment(double amount);
    void refundPayment(double amount); // Added this new method
}

The moment I hit save, my IDE turns bright red. Why? Because CreditCardProcessor and PayPalProcessor no longer "satisfy the contract." They both claim to be a PaymentProcessor, but neither of them knows how to refundPayment. The compiler is screaming at me to implement this new method in every single class that uses the interface.

In a small project, this is a nuisance. In a massive enterprise codebase with hundreds of implementations, this is a nightmare. You can't just "push the update" because you'd have to rewrite dozens of classes just to keep the project compiling.

The 'default' escape hatch

This is exactly why Java introduced default methods. Instead of forcing every implementation to provide their own logic, I can provide a "fallback" implementation directly inside the interface using the default keyword.

Let's try this. I'll modify the interface to provide a default behavior for refunds—maybe just logging that a refund was requested for now.

public interface PaymentProcessor {
    void processPayment(double amount);

    default void refundPayment(double amount) {
        System.out.println("Default refund logic: Requesting refund of $" + amount + " via admin portal.");
    }
}

And just like that, the red error lines in my CreditCardProcessor and PayPalProcessor classes vanish. I didn't touch a single line of code in those classes, yet they now "have" a refundPayment method. They are inheriting the default behavior from the interface.

When the default isn't enough

Now, here is the catch: default methods aren't meant to be a lazy way to avoid writing code; they're meant to provide a sensible baseline. In the real world, PayPal probably has a very specific API for refunds that is different from our "admin portal" default.

I can still override the default method in any class that needs a specialized version. Let's adjust the PayPalProcessor:

public class PayPalProcessor implements PaymentProcessor {
    public void processPayment(double amount) {
        System.out.println("Processing PayPal payment of $" + amount);
    }

    @Override
    public void refundPayment(double amount) {
        System.out.println("Using PayPal API to instantly refund $" + amount);
    }
}

Now, if I call refundPayment on a CreditCardProcessor, it uses the generic "admin portal" logic. But if I call it on the PayPalProcessor, it uses the specific API logic. I've evolved my API without breaking existing code, while still allowing for customization where it actually matters.




📋 Practical Task

Exercise: Building a Notification Dispatcher with Opt-out Defaults

You are tasked with creating a notification system that can send alerts via different channels (Email, SMS, Slack). Some channels are "urgent" and need a specific priority flag, while others don't.

Requirements:

  • Create an interface named NotificationChannel.
  • Add a required method: void send(String message).
  • Add a default method named sendWithPriority(String message, int priority). The default implementation should simply print: "Sending standard priority message: [message]".
  • Create a class EmailNotification that implements NotificationChannel and provides the send logic. It should use the default priority logic.
  • Create a class SmsNotification that implements NotificationChannel. It must provide the send logic and override sendWithPriority to print: "Sending URGENT SMS (Priority [priority]): [message]".
  • In a Main class, create a list of NotificationChannel objects containing both an Email and an SMS notification. Iterate through the list and call sendWithPriority("System Update", 1) on each to verify that one uses the default and the other uses the override.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.