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
109: Sealed Classes and Interfaces
For a long time in Java, we had a binary choice when designing class hierarchies: either you made a class final, meaning nobody could ever extend it, or you left it open, meaning any class in any package could potentially subclass it. This is often too blunt an instrument. Sometimes you want to say, "I'm okay with these three specific classes extending my base, but I don't want some random developer in a different module adding a fourth one that breaks my business logic."
Restricting the Payment Hierarchy
Let's build a small payment processing system. In this scenario, I only support three payment methods: Credit Card, PayPal, and Bank Transfer. If I use a standard interface, anyone can implement PaymentMethod. But if I'm using a switch expression to handle the logic for each type, an unexpected new implementation would cause a runtime error or force me to rely on a generic default case that might hide bugs.
Here is how I'll start. I'll define a sealed interface. By using the sealed modifier and the permits clause, I'm explicitly telling the Java compiler exactly who is allowed in the club.
public sealed interface PaymentMethod permits CreditCard, PayPal, BankTransfer {
double amount();
}
Defining our Permitted Methods
Now I need to create the classes I just promised the interface. There's a catch here that often trips people up: every class that extends a sealed class must explicitly declare its own "seal status." It has to be either final (no more subclasses), sealed (further restricted subclasses), or non-sealed (opening it back up to the world).
Since these payment methods are pretty concrete, I'll make them all final.
public final class CreditCard implements PaymentMethod {
private final double amount;
public CreditCard(double amount) { this.amount = amount; }
public double amount() { return amount; }
}
public final class PayPal implements PaymentMethod {
private final double amount;
public PayPal(double amount) { this.amount = amount; }
public double amount() { return amount; }
}
public final class BankTransfer implements PaymentMethod {
private final double amount;
public BankTransfer(double amount) { this.amount = amount; }
public double amount() { return amount; }
}
Hitting the Compiler's Wall
I'll be honest—when I first started using sealed classes, I forgot about the modifier requirement on the subclasses. I tried to write the CreditCard class as a plain public class CreditCard implements PaymentMethod. The compiler immediately yelled at me, complaining that the class must be declared final, sealed, or non-sealed.
It felt like extra boilerplate at first, but then I realized why it's there. If the compiler didn't force me to choose, the "seal" would be meaningless. If CreditCard was just a regular class, someone could create a SpecialCreditCard that extends CreditCard, and suddenly my sealed hierarchy has a leak. Java forces you to be explicit about whether the chain of restriction ends or continues.
The Payoff: Exhaustive Switching
The real magic happens when you use these classes in a switch. Because the compiler knows exactly which classes implement PaymentMethod, it can check if you've covered all possible cases. If I miss one, the code won't even compile.
Check out how clean this looks compared to a chain of instanceof checks:
public class PaymentProcessor {
public String process(PaymentMethod method) {
return switch (method) {
case CreditCard cc -> "Processing credit card payment of $" + cc.amount();
case PayPal pp -> "Redirecting to PayPal for $" + pp.amount();
case BankTransfer bt -> "Scheduling bank transfer of $" + bt.amount();
// No default case needed! The compiler knows these are the only three.
};
}
}
If I were to add a CryptoPayment class to the permits list later, the process method would immediately throw a compile-time error. This is a huge safety win; the compiler effectively becomes my checklist, ensuring I don't forget to update my business logic when the data model changes.
📋 Practical Task
Implement a Sealed Notification System
You are building a notification dispatcher. You need to ensure that only three types of notifications can exist in the system: Email, SMS, and PushNotification. No other notification types should be possible.
- Create a sealed interface named
Notificationthat permitsEmail,SMS, andPushNotification. - Each notification type should be a
finalclass containing aString message. - Write a class called
NotificationServicewith a methodsend(Notification n). - Inside the
sendmethod, use aswitchexpression to return a unique string for each notification type (e.g., "Sending Email: [message]"). - Ensure that you do not use a
defaultcase in your switch expression, relying instead on the exhaustiveness of the sealed interface.
There are no comments for now.