Back to Blog
Java

Java Sealed Interface Implementation

java sealed interface implementation: Learn how to implement sealed interfaces in Java: syntax, permits clause, restrictions, and practical use cases for exhaustive pa...

sealed interfacesJava 17type hierarchypattern matchingAPI design
A sealed interface with a finite set of permitted implementations, shown as a closed box with labeled compartments.

Java sealed interface implementation restricts which types can implement an interface, giving the compiler enough information to verify exhaustive pattern matching. Sealed interfaces were introduced in Java 17 as part of the sealed types feature. They let you declare a finite set of permitted implementations, which is useful when modeling domain hierarchies where all possible subtypes are known at compile time.

Declaring a Sealed Interface

The core syntax for a sealed interface uses the sealed modifier and a permits clause that lists every direct implementation. For example:

public sealed interface Shape permits Circle, Rectangle, Triangle { double area(); }

Each permitted class must be in the same module or, if the interface is in an unnamed module, in the same package. The permitted subclasses must directly implement the interface; indirect implementations are not allowed. Each permitted class must also declare one of three modifiers: final, sealed, or non-sealed.

public final class Circle implements Shape { private final double radius; public Circle(double radius) { this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } } public sealed class Rectangle implements Shape permits Square { // ... } public non-sealed class Triangle implements Shape { // ... }

A final class ends the hierarchy for that branch. A sealed class can itself have permitted subclasses. A non-sealed class reopens the hierarchy, allowing any subclass without further restrictions. This gives you fine control over how much of the type hierarchy is closed.

Rules and Restrictions for Sealed Implementations

Sealed interfaces enforce several rules at compile time. The permits list must include every direct subclass; the compiler rejects any missing or extra entry. Also, each permitted subclass must be accessible from the interface's location, meaning it must be in the same module or package. If you try to implement a sealed interface without listing the class in permits, compilation fails with an error like class is not allowed to extend sealed interface.

Another restriction is that the permitted subclasses must be direct. You cannot have a class that implements a sealed interface indirectly through another class. The hierarchy is strictly one level deep unless you use sealed or non-sealed to extend it further.

Sealed Interfaces vs Sealed Classes

Sealed interfaces and sealed classes share the same rules for permits and subclass modifiers. The difference is conceptual: interfaces define contracts without state, while classes can carry implementation and fields. In practice, sealed interfaces are often preferred for pure abstractions, especially when you want to support multiple inheritance of types. Sealed classes are useful when the hierarchy needs shared state or default method implementations.

AspectSealed InterfaceSealed Class
StateNo instance fieldsCan have instance fields
Multiple typesA class can implement several sealed interfacesA class can extend only one sealed class
Default methodsYes, via default methodsYes, via regular methods
Typical useContracts, data modeling, DTOsBase classes with shared logic

Choose a sealed interface when the hierarchy is about capability, not implementation. Choose a sealed class when subclasses should inherit concrete behavior or fields.

Exhaustive Pattern Matching with Sealed Interfaces

One of the main benefits of a sealed interface is that the compiler can prove that a switch expression covers all possible subtypes. With Java 21's pattern matching for switch, you can write:

public String describe(Shape shape) { return switch (shape) { case Circle c -> "Circle with radius " + c.radius(); case Rectangle r -> "Rectangle with area " + r.area(); case Triangle t -> "Triangle"; }; }

Because Shape is sealed, the compiler knows these three cases are exhaustive. You do not need a default branch. If you later add a new permitted type, the switch will fail to compile until you handle it. This shifts errors from runtime to compile time, which is a significant maintainability advantage.

For instanceof chains, sealed interfaces also enable exhaustive checks when used with pattern matching. The compiler can verify that a series of instanceof tests covers all permitted types, though this is less common than switch expressions.

Common Mistakes and Edge Cases

A frequent mistake is forgetting to mark a permitted subclass as final, sealed, or non-sealed. The compiler rejects the class with an error like sealed, non-sealed or final modifiers expected. Another mistake is listing a subclass in permits that is not directly implementing the interface. For example, if Square extends Rectangle and Rectangle is sealed, you cannot list Square in Shape's permits; it must be listed in Rectangle's permits.

Edge cases arise with records. A record is implicitly final, so it can be a permitted subclass without an explicit modifier:

public record Point(double x, double y) implements Shape { @Override public double area() { return 0; } }

This works because records are final by design. Similarly, enums are implicitly final and can implement a sealed interface, though this is less common.

Maintainability and API Design Considerations

Sealed interfaces are a design tool for controlling extensibility. Use them when you own the complete set of implementations and want to prevent external code from adding new ones. This is common in domain modeling, such as representing financial instruments, AST nodes, or UI elements. The sealed hierarchy makes the code easier to reason about because the compiler enforces a closed set.

However, sealing an interface is a commitment. Adding a new permitted type later is a breaking change for any switch that relies on exhaustiveness. If your API is intended to be extended by clients, a sealed interface may be too restrictive. In that case, a regular interface or an abstract class with a public constructor is more appropriate. The decision hinges on whether you can foresee all implementations at design time.

Sealed interfaces also improve documentation. The permits clause acts as a declarative list of known implementations, making the intended hierarchy explicit. This is especially valuable in large codebases where discoverability matters.

Runtime Behavior and Reflection

The sealed information is stored in the class file as part of the PermittedSubclasses attribute. At runtime, you can inspect it via reflection using Class.getPermittedSubclasses(). This method returns an array of Class<?> objects representing the direct subclasses, or null if the class is not sealed. This is useful for frameworks that need to discover implementations dynamically, such as serialization libraries or dependency injection containers.

There is no runtime performance overhead from sealing. The compiler enforces all rules statically; the JVM does not check sealed relationships at runtime. The PermittedSubclasses attribute is only metadata. This means you can use sealed interfaces freely in performance-sensitive code without worrying about reflection costs unless you explicitly call getPermittedSubclasses().

One subtle behavior is that the order of classes in the permits clause is preserved in the class file. If your code relies on that order, it is stable as long as you do not recompile. However, you should not depend on it for logic; treat it as informational.

When a sealed interface is used with records, the record's components are also available via reflection, but the sealed relationship itself does not change how records behave. The same rules apply: records are final, so they can be direct permitted implementations without extra modifiers.

For developers working with libraries that generate code or proxies, sealed interfaces can be restrictive. A dynamic proxy cannot implement a sealed interface unless it is explicitly listed in permits, which is impossible for external code. If you need proxying or runtime subclassing, avoid sealing the interface. This is a practical constraint to weigh when designing an API that may be used with frameworks like Spring or Hibernate.

Sealed interfaces are a mature feature in modern Java. They integrate cleanly with pattern matching and records, making them a solid choice for closed hierarchies. The key is to use them deliberately, understanding that they trade flexibility for compile-time safety.

java sealed interface implementation: Practical Usage and Co | RYUSLOG DEV