java

Java Sealed Classes Guide: Complete Inheritance Control and Pattern Matching in Java 17

Master Java 17 sealed classes for precise inheritance control. Learn to restrict subclasses, enable exhaustive pattern matching, and build robust domain models. Get practical examples and best practices.

Java Sealed Classes Guide: Complete Inheritance Control and Pattern Matching in Java 17

Java sealed classes, introduced as a preview feature and finalized in Java 17, offer precise control over inheritance hierarchies. They let us define which classes can extend or implement a type, preventing arbitrary inheritance. This technique enhances domain modeling by making hierarchies explicit and compiler-verifiable. I’ve found this particularly valuable when designing APIs or domain-specific rules where unchecked inheritance could lead to fragility.

Basic Sealed Class Declaration
Declaring a sealed class requires explicitly listing permitted subclasses. This creates a bounded inheritance model. For example, in payment processing systems, I restrict payment methods to known types:

public sealed class PaymentMethod permits CreditCard, PayPal, BankTransfer {  
    public abstract void process();  
}  

public final class CreditCard extends PaymentMethod {  
    @Override public void process() {  
        System.out.println("Processing credit card");  
    }  
}  
// Compile-time error if unpermitted subclass attempted  

The compiler enforces that only CreditCard, PayPal, and BankTransfer can extend PaymentMethod. This eliminates accidental extensions and clarifies valid variants.

Sealed Interfaces with Records
Sealed interfaces pair well with records for concise data hierarchies. When modeling log entries:

public sealed interface LogEntry permits SystemLog, UserLog, AuditLog {  
    String getMessage();  
}  

public record SystemLog(String component, String message) implements LogEntry {  
    public String getMessage() { return "[" + component + "] " + message; }  
}  

public record UserLog(String userId, String action) implements LogEntry {  
    public String getMessage() { return userId + " performed: " + action; }  
}  

Here, records implement the interface cleanly while the sealed constraint ensures no unexpected log types appear later.

Local Permitted Subclasses
For self-contained hierarchies, nest permitted subclasses within the sealed parent:

public sealed abstract class FileFormat {  
    public static final class CSV extends FileFormat {  
        public void parse() { /* CSV logic */ }  
    }  
    public static final class JSON extends FileFormat {  
        public void parse() { /* JSON logic */ }  
    }  
    private FileFormat() {} // Block external inheritance  
}  

// Usage: only CSV/JSON allowed  
FileFormat format = new FileFormat.CSV();  

The private constructor prevents extension outside the class. I use this for configuration objects where variants are fixed.

Controlled Openness with Non-Sealed
Use non-sealed for partial hierarchy openness. In device modeling:

public sealed class Device permits Mobile, Computer {  
    public abstract String getOS();  
}  

public non-sealed abstract class Mobile extends Device {}  

public final class Android extends Mobile {  
    @Override public String getOS() { return "Android"; }  
}  

public final class IOS extends Mobile {  
    @Override public String getOS() { return "iOS"; }  
}  

Mobile is non-sealed, allowing Android and IOS extensions. Computer remains sealed. This balances flexibility with constraint.

Exhaustive Pattern Matching
Sealed classes enable compiler-checked exhaustiveness in pattern matching. For geometry calculations:

public sealed interface Shape permits Circle, Rectangle {}  
public record Circle(double radius) implements Shape {}  
public record Rectangle(double width, double height) implements Shape {}  

public double calculateArea(Shape shape) {  
    return switch (shape) {  
        case Circle c -> Math.PI * c.radius() * c.radius();  
        case Rectangle r -> r.width() * r.height();  
    };  
}  

If I add a Triangle to permits, the switch immediately fails compilation until I handle the new case. This catches errors early.

Sealed Records for Data Trees
Model recursive structures like trees with sealed records:

public sealed interface Tree<T> permits Leaf, Node {}  
public record Leaf<T>(T value) implements Tree<T> {}  
public record Node<T>(Tree<T> left, Tree<T> right) implements Tree<T> {}  

// Build a tree: Node(Leaf(1), Node(Leaf(2), Leaf(3)))  
Tree<Integer> tree = new Node<>(new Leaf<>(1), new Node<>(new Leaf<>(2), new Leaf<>(3)));  

Records provide immutable data carriers while the sealed hierarchy guarantees only Leaf and Node exist. I use this for ASTs in compilers.

Factory Methods with Sealed Results
Combine static factories with sealed classes for expressive APIs:

public sealed class Result<T> permits Result.Success, Result.Failure {  
    public static final class Success<T> extends Result<T> {  
        private final T value;  
        Success(T value) { this.value = value; }  
    }  
    public static final class Failure<T> extends Result<T> {  
        private final Exception error;  
        Failure(Exception error) { this.error = error; }  
    }  
    
    public static <T> Result<T> success(T value) {  
        return new Success<>(value);  
    }  
    public static <T> Result<T> failure(Exception e) {  
        return new Failure<>(e);  
    }  
}  

// Usage: clear success/failure paths  
Result<String> data = Result.success("Processed");  

Clients handle success/failure without worrying about unknown subtypes.

Reflection for Runtime Checks
Inspect sealed hierarchies at runtime using reflection:

public void validatePlugin(Class<?> pluginClass) {  
    if (pluginClass.isSealed()) {  
        Class<?>[] permitted = pluginClass.getPermittedSubclasses();  
        System.out.println("Allowed plugins: " + Arrays.toString(permitted));  
    }  
}  

// Check LogEntry from earlier:  
validatePlugin(LogEntry.class); // Prints: [SystemLog, UserLog, AuditLog]  

This helps in frameworks that dynamically load modules, ensuring only permitted types are instantiated.

Framework Integration
Enforce plugin contracts in extensible systems:

public sealed interface Plugin permits DatabasePlugin, AuthPlugin {  
    void initialize();  
}  

public final class DatabasePlugin implements Plugin {  
    @Override public void initialize() { connectToDB(); }  
}  

public Plugin loadPlugin(String type) {  
    return switch (type) {  
        case "db" -> new DatabasePlugin();  
        case "auth" -> new AuthPlugin();  
        default -> throw new IllegalArgumentException("Invalid plugin");  
    };  
}  

The sealed interface acts as a “gatekeeper,” preventing invalid plugins from being loaded.

Migrating Final Classes
Gradually relax overly strict hierarchies:

// Before: closed for extension  
public final class Cat extends Animal {}  
public final class Dog extends Animal {}  

// After: allow controlled extension  
public sealed class Animal permits Cat, Dog, Bird {}  
public non-sealed class Bird extends Animal {}  
public final class Parrot extends Bird {} // Now permitted  

Existing Cat/Dog remain final, while Bird opens for specialization like Parrot. This maintains backward compatibility.

Java sealed classes bring intentionality to inheritance. By declaring permitted subtypes upfront, we create self-documenting hierarchies. The compiler becomes an ally, verifying exhaustiveness in pattern matching and blocking unauthorized extensions. For domain modeling, this means business rules like “only these payment methods are allowed” become explicit in code. Performance-wise, sealed classes enable efficient pattern matching as the JVM can optimize based on known subtypes. When designing APIs, I use them to prevent client misuse while retaining evolution paths through non-sealed branches. They turn inheritance from a potential maintenance liability into a structured design tool.

Keywords: java sealed classes, sealed classes java 17, java sealed interface, java sealed records, sealed class example java, java sealed class tutorial, sealed classes vs final classes, java pattern matching sealed classes, sealed interface java example, java sealed class inheritance, sealed classes java tutorial, java 17 sealed classes, sealed class java syntax, java sealed class permits, sealed records java, java sealed class benefits, sealed class factory pattern java, java sealed hierarchy, sealed classes pattern matching, java sealed class reflection, sealed class vs enum java, java sealed class migration, sealed interface records java, java sealed class design patterns, sealed classes java best practices, java sealed class non-sealed, sealed class switch expression java, java sealed class API design, sealed classes domain modeling, java sealed class compiler, sealed class exhaustive matching java, java sealed class performance, sealed classes inheritance control, java sealed class polymorphism, sealed interface implementation java, java sealed class validation, sealed classes java features, java sealed class framework, sealed class permitted subclasses, java sealed class runtime, sealed classes java programming, java sealed class examples, sealed interface java 17, java sealed class switch, sealed records pattern matching, java sealed class hierarchy design, sealed classes java syntax, java sealed class OOP, sealed interface design java, java sealed class type safety



Similar Posts
Blog Image
Advanced Debug Logging Patterns: Best Practices for Production Applications [2024 Guide]

Learn essential debug logging patterns for production Java applications. Includes structured JSON logging, MDC tracking, async logging, and performance monitoring with practical code examples.

Blog Image
How Can You Turn Your Java App into a Fort Knox with Spring Security and OAuth2?

Java Fortress Building: Using Spring Security and OAuth2 for Ultimate Protection

Blog Image
Ready to Turbocharge Your Java Apps with Parallel Streams?

Unleashing Java Streams: Amp Up Data Processing While Keeping It Cool

Blog Image
Mastering Java's Optional API: 15 Advanced Techniques for Robust Code

Discover powerful Java Optional API techniques for robust, null-safe code. Learn to handle nullable values, improve readability, and enhance error management. Boost your Java skills now!

Blog Image
5 Essential Java Concurrency Patterns for Robust Multithreaded Applications

Discover 5 essential Java concurrency patterns for robust multithreaded apps. Learn to implement Thread-Safe Singleton, Producer-Consumer, Read-Write Lock, Fork/Join, and CompletableFuture. Boost your coding skills now!

Blog Image
Master Java Testing: 10 Essential Techniques for Robust Applications

Discover effective Java testing strategies using JUnit 5, Mockito, and Spring Boot. Learn practical techniques for unit, integration, and performance testing to build reliable applications with confidence. #JavaTesting #QualityCode