Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
31: Interfaces and Default Methods
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
EmailNotificationthat implementsNotificationChanneland provides thesendlogic. It should use the default priority logic. - Create a class
SmsNotificationthat implementsNotificationChannel. It must provide thesendlogic and overridesendWithPriorityto print:"Sending URGENT SMS (Priority [priority]): [message]". - In a
Mainclass, create a list ofNotificationChannelobjects containing both an Email and an SMS notification. Iterate through the list and callsendWithPriority("System Update", 1)on each to verify that one uses the default and the other uses the override.
There are no comments for now.