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
Brewing Java Magic with Micronaut and MongoDB

Dancing with Data: Simplifying Java Apps with Micronaut and MongoDB

Blog Image
Unleash Rust's Hidden Concurrency Powers: Exotic Primitives for Blazing-Fast Parallel Code

Rust's advanced concurrency tools offer powerful options beyond mutexes and channels. Parking_lot provides faster alternatives to standard synchronization primitives. Crossbeam offers epoch-based memory reclamation and lock-free data structures. Lock-free and wait-free algorithms enhance performance in high-contention scenarios. Message passing and specialized primitives like barriers and sharded locks enable scalable concurrent systems.

Blog Image
10 Essential Java Features Since Version 9: Boost Your Productivity

Discover 10 essential Java features since version 9. Learn how modules, var, switch expressions, and more can enhance your code. Boost productivity and performance now!

Blog Image
Level Up Your Java Testing Game with Docker Magic

Sailing into Seamless Testing: How Docker and Testcontainers Transform Java Integration Testing Adventures

Blog Image
Java's invokedynamic: Supercharge Your Code with Runtime Method Calls

Java's invokedynamic instruction allows method calls to be determined at runtime, enabling dynamic behavior and flexibility. It powers features like lambda expressions and method references, enhances performance for dynamic languages on the JVM, and opens up possibilities for metaprogramming. This powerful tool changes how developers think about method invocation and code adaptability in Java.

Blog Image
How Can JMX Be the Swiss Army Knife for Your Java Applications?

Unlocking Java’s Secret Toolkit for Seamless Application Management