Back to Blog
Java

Understanding the Java Permits Keyword in Sealed Classes

java permits keyword: Learn how the Java permits keyword defines allowed subclasses in sealed classes, including syntax, constraints, and runtime behavior.

sealed classespermits clauseJava 17pattern matchinginheritance control
Illustration of a sealed class with a permits clause restricting subclasses to a fixed set.

The java permits keyword appears in sealed class and interface declarations to specify exactly which classes may extend or implement them. Sealed types, introduced in Java 17, give developers explicit control over inheritance hierarchies. Instead of relying on final to prevent all subclassing or leaving a class open to arbitrary extension, a sealed class defines a closed set of permitted subclasses.

Declaring a Sealed Class with Permits

A sealed class uses the sealed modifier and a permits clause to list its direct subclasses. Each listed class must be in the same module or package unless the sealed class is in a named module, in which case the permitted subclasses can be in different packages of that module.

public sealed class Shape permits Circle, Rectangle, Triangle { // common shape behavior }

Here, Shape is sealed, and only Circle, Rectangle, and Triangle can extend it. Any other attempt to subclass Shape results in a compile-time error. The permitted subclasses must be direct subclasses; no intermediate classes are allowed in the permits list.

The Permits Clause Must List All Direct Subclasses

If a class is sealed, every direct subclass must appear in the permits clause. The compiler enforces this. If you forget one, the class will not compile. This requirement makes the inheritance hierarchy explicit and auditable.

public sealed class Vehicle permits Car, Truck { } public final class Car extends Vehicle { } public final class Truck extends Vehicle { }

If you later add another subclass, you must update the permits clause. This is a deliberate design choice: the API author controls the set of subtypes, which is useful for domain modeling where the set of variants is known and fixed.

Sealed Interface and Permits Behavior

Interfaces can also be sealed. The syntax is identical, and the permitted types are the interfaces or classes that may implement or extend the sealed interface.

public sealed interface JsonValue permits JsonString, JsonNumber, JsonObject { } public final class JsonString implements JsonValue { } public final class JsonNumber implements JsonValue { } public final class JsonObject implements JsonValue { }

Sealed interfaces work well with pattern matching, especially when combined with the switch expression. Because the set of implementations is closed, the compiler can verify exhaustiveness at compile time.

public String describe(JsonValue value) { return switch (value) { case JsonString s -> "string: " + s.value(); case JsonNumber n -> "number: " + n.value(); case JsonObject o -> "object with " + o.size() + " fields"; }; }

Without a sealed type, this switch would require a default branch or a null check. With sealed types, the compiler knows all possible subtypes and can confirm that every case is covered.

Constraints on Sealed Classes and Their Subclasses

Permitted subclasses must themselves be declared with one of three modifiers: final, sealed, or non-sealed. This rule ensures that the hierarchy remains controlled.

  • final ends the inheritance chain for that subclass.
  • sealed continues the restriction, allowing further subclasses only if they are listed in that subclass's own permits clause.
  • non-sealed opens the subclass to arbitrary extension, effectively breaking the sealed chain at that point.
public sealed class Node permits Leaf, Branch { } public final class Leaf extends Node { } public non-sealed class Branch extends Node { }

Here, Branch is non-sealed, so any class can extend Branch without restriction. This is useful when a particular variant needs to remain extensible while the root type stays sealed.

The permitted subclass must be accessible from the sealed class. If the sealed class and its subclasses are in different packages, the subclasses must be public or protected and the sealed class must be in a named module. In an unnamed module, they must be in the same package.

Runtime Behavior and Reflection with Sealed Types

The permits clause is not just a compile-time construct; it is recorded in the class file and visible at runtime. The Class class provides getPermittedSubclasses(), which returns an array of Class<?> objects representing the direct permitted subclasses.

Class<Shape> shapeClass = Shape.class; Class<?>[] permitted = shapeClass.getPermittedSubclasses(); System.out.println(Arrays.toString(permitted));

This reflection API allows frameworks and libraries to inspect sealed hierarchies dynamically. For example, a serialization library can enumerate all possible subtypes without scanning the classpath. The runtime also enforces the sealed constraint: attempting to define a subclass that is not in the permitted list throws an IncompatibleClassChangeError when the class is loaded.

This runtime enforcement has implications for security-sensitive code. Sealed types provide a stronger guarantee than final alone because they allow a controlled set of subtypes while still preventing arbitrary extension. However, reflection can still access private members, so sealed types do not replace access control.

Common Mistakes and Compile-Time Errors

A frequent error is omitting a subclass from the permits clause. The compiler reports something like: class is not allowed to extend sealed class. Another mistake is using a class that is not final, sealed, or non-sealed as a permitted subclass. The subclass must explicitly declare one of these modifiers.

public sealed class Result permits Success, Failure { } public class Success extends Result { // error: Success must be final, sealed, or non-sealed }

You also cannot use an anonymous class or a lambda to extend a sealed class, because those constructs do not have a name that can appear in the permits clause. Similarly, a permitted subclass cannot be an inner class unless it is explicitly listed by its fully qualified name.

Another subtle issue arises when a sealed class is in a named module and the permitted subclass is in a different package. The subclass must be exported, and the sealed class must have a permits clause that references the subclass by its canonical name. If the subclass is not accessible, the compiler rejects the declaration.

Choosing Between Sealed Classes and Other Abstraction Mechanisms

Sealed classes occupy a middle ground between final classes and open inheritance. The following table summarizes the key differences:

MechanismSubclassingUse case
final classNo subclasses allowedImmutable value types, utility classes
Sealed classOnly listed subclassesFixed domain models, algebraic data types
Non-sealed classAny subclass allowedExtension points, framework hooks

Use sealed classes when the set of possible subtypes is known at compile time and you want to enforce that invariant. Typical examples include AST nodes, JSON values, arithmetic expressions, or state machines. The compiler's exhaustiveness checking for switch patterns makes sealed types especially valuable in these scenarios.

If you need to allow third-party extensions, a non-sealed class or an interface without sealed restrictions is more appropriate. Sealed types are not a replacement for access modifiers or module encapsulation; they complement them by controlling the type hierarchy itself.

Sealed classes also affect maintainability. Because the hierarchy is closed, adding a new subtype requires modifying the sealed parent and updating all pattern-matching switches. This is a deliberate tradeoff: you gain compile-time safety but lose the ability to extend the hierarchy without changing the original code. In a library, this can be a breaking change for consumers, so the sealed set should be stable and well-documented.

When performance matters, sealed types can enable optimizations in pattern matching. The JVM can use the closed set to generate more efficient dispatch code, though the exact behavior depends on the runtime version and the shape of the switch. The primary benefit is correctness, not speed. For most applications, the compile-time guarantees outweigh any micro-optimizations.

Finally, consider how sealed types interact with serialization and object graphs. A sealed hierarchy makes it easier to validate incoming data because the set of allowed types is known. But reflection-based serializers must handle the permitted subclasses explicitly, and any class not in the list will fail at runtime. Ensure that your serialization layer accounts for sealed types, especially when evolving the hierarchy over time.

java permits keyword: Practical Usage and Code Examples | RYUSLOG DEV