Sealed classes and sealed interfaces in Java list the classes that may extend or implement them, and the compiler rejects every other subtype. We declare a sealed type with the sealed modifier and name the allowed subtypes in a permits clause.
We use sealed types to model a fixed set of cases, such as the outcomes of a payment or the kinds of messages an API returns. Because the compiler knows every subtype, a switch over a sealed type can cover all cases without a default branch, and adding a new case shows every switch that needs an update.
The following example declares a sealed interface with two record implementations and switches over it.
sealed interface PaymentResult permits Paid, Declined {}
record Paid(double amount) implements PaymentResult {}
record Declined(String reason) implements PaymentResult {}
static String describe(PaymentResult result) {
return switch (result) {
case Paid p -> "Paid " + p.amount();
case Declined d -> "Declined, " + d.reason();
};
}
String ok = describe(new Paid(25.0)); // "Paid 25.0"
String failed = describe(new Declined("card expired")); // "Declined, card expired"
Notice that the switch has no default case, and it still compiles, because Paid and Declined are the only possible types. The next sections cover the permits clause, the three modifiers for subclasses, where subclasses must live, sealed interfaces with records, exhaustive switch, and the reflection API.
1. Why Restrict Inheritance?
In plain Java, any class that is not final can be extended by any other class that can see it. The access modifiers control who can use a class, but not who can extend it. A final class blocks every subclass, which is too strict when we want a few known ones.
Sealed types fill the gap between the two. A sealed class can be public and used everywhere, while only the classes in its permits list can extend it.
| Declaration | Who can extend it |
|---|---|
| class Pass | any class that can see it |
| final class Pass | no class |
| sealed class Pass permits A, B | only A and B |
| non-sealed class A extends Pass | any class again, below A |
Say a gym app sells day passes, monthly passes and custom passes for companies. The price calculation and the access check assume these three kinds only. If another team adds a fourth pass type by extending the base class, the code compiles and the new type falls into a wrong else branch at runtime. A sealed class turns that mistake into a compile error.
2. Declaring a Sealed Class With permits
The sealed modifier goes before class, and the permits clause comes after any extends and implements clauses. Each permitted subclass must be a direct subclass of the sealed class.

sealed abstract class Pass permits DayPass, MonthPass, CustomPass {
abstract double price();
}
final class DayPass extends Pass {
double price() { return 12; }
}
sealed class MonthPass extends Pass permits StudentMonthPass {
double price() { return 60; }
}
final class StudentMonthPass extends MonthPass {
double price() { return 40; }
}
non-sealed class CustomPass extends Pass {
double price() { return 100; }
}
class CorporatePass extends CustomPass {
double price() { return 80; }
}
double day = new DayPass().price(); // 12.0
double student = new StudentMonthPass().price(); // 40.0
double corporate = new CorporatePass().price(); // 80.0
A class that is not in the permits list does not compile, even when it is in the same package.
// does not compile: class is not allowed to extend sealed class: Pass (as it is not listed in its 'permits' clause)
final class TrialPass extends Pass {
double price() { return 0; }
}
3. Subclass Modifiers final, sealed and non-sealed
Every permitted subclass must declare one of the modifiers final, sealed or non-sealed, and only one. The modifier says how the subclass continues the restriction of its parent, and the compiler reports “sealed, non-sealed or final modifiers expected” when it is missing.
| Modifier on the subclass | Effect | In the example |
|---|---|---|
| final | No further subclasses | DayPass, StudentMonthPass |
| sealed | Subclasses only from its own permits list | MonthPass |
| non-sealed | Open again, any class may extend it | CustomPass |
The non-sealed modifier is a deliberate opening in the hierarchy. The sealed parent still knows its three direct subclasses, but code that switches over Pass has to treat CustomPass and all its unknown subclasses as one case. Records are implicitly final, and enums are implicitly final or sealed, so they need no modifier when they are permitted subtypes.
4. Where Permitted Subclasses Must Live
The compiler needs to see the whole hierarchy, so the rules for the location of permitted subclasses are strict.
- In a named module, every permitted subclass must be in the same module as the sealed class.
- In the unnamed module, which is the classpath, every permitted subclass must be in the same package.
- When all subclasses are declared in the same source file as the sealed class, we can leave out the permits clause, and the compiler infers the list.
- Local classes and anonymous classes can never extend a sealed type. The compiler reports “anonymous classes must not extend sealed classes”.
// permits is inferred: both records are in the same file
sealed interface Discount {}
record Percent(int value) implements Discount {}
record Fixed(double amount) implements Discount {}
Leaving out permits is common for small hierarchies of records. For a hierarchy spread over several files, the explicit list documents the design in one place.
5. Sealed Interfaces and Records
A sealed interface works the same way as a sealed class. Its permits clause lists the classes that may implement it and the interfaces that may extend it, and each of them again needs final, sealed or non-sealed, unless it is a record or an enum.
Sealed interfaces and records fit together well. The interface defines the set of cases, and each record holds the data of one case, which gives Java the algebraic data types known from other languages. The PaymentResult type from the intro is this pattern, and an enum can join the same hierarchy when a case has no data.
sealed interface Delivery permits Courier, Pickup {}
record Courier(String address, int days) implements Delivery {}
enum Pickup implements Delivery { STORE, LOCKER }
Delivery fast = new Courier("Main St 5", 1);
Delivery locker = Pickup.LOCKER;
boolean isRecord = fast.getClass().isRecord(); // true
6. Exhaustive switch Over a Sealed Type
The main payoff of sealed types arrives with pattern matching for switch, standard since Java 21. A switch expression must cover every possible value, and for a sealed type it is enough to cover every permitted subtype.
static int etaDays(Delivery delivery) {
return switch (delivery) {
case Courier c -> c.days();
case Pickup p -> 0;
};
}
int courierEta = etaDays(new Courier("Main St 5", 2)); // 2
int pickupEta = etaDays(Pickup.STORE); // 0
When we add a third record, such as Drone, to the permits list, every switch expression over Delivery stops compiling until it handles the new case. A default branch would hide that error, so we leave it out on purpose.
// does not compile: the switch expression does not cover all possible input values
sealed interface Delivery permits Courier, Pickup, Drone {}
record Drone(int minutes) implements Delivery {}
static int etaDays(Delivery delivery) {
return switch (delivery) {
case Courier c -> c.days();
case Pickup p -> 0;
};
}
The same check works with record patterns, which take the components apart in the case label. With an if chain of instanceof patterns, the compiler does not check that all cases are covered, so a switch is the better choice.
7. Sealed Types and Reflection
The class java.lang.Class has two methods for sealed types, added in Java 17. The method isSealed() returns true for a sealed class or interface, and getPermittedSubclasses() returns the permitted subtypes, or null when the type is not sealed.
boolean sealedType = PaymentResult.class.isSealed(); // true
int permittedCount = PaymentResult.class.getPermittedSubclasses().length; // 2
boolean openType = String.class.getPermittedSubclasses() == null; // true
The order of the returned array is unspecified, so code must not depend on it. Code that needs the list of cases at runtime can use these methods, for example a unit test that checks that every permitted subtype has a message template.
8. Version History of Sealed Classes
Sealed classes went through two preview rounds before they became a standard feature, which explains why older tutorials still mention –enable-preview. The other language changes of each release are summed up in Java features by version.
| Java version | Change | JEP |
|---|---|---|
| Java 15 | Sealed classes in preview | JEP 360 |
| Java 16 | Second preview | JEP 397 |
| Java 17 | Standard feature, no flags needed | JEP 409 |
| Java 21 | Pattern matching for switch checks sealed hierarchies | JEP 441 |
9. Sealed Classes FAQs
Developers who start with sealed types often search for these questions.
9.1. What Is the Difference Between sealed and final?
A final class has no subclasses at all, while a sealed class has a fixed list of subclasses. We use final for a class that is complete on its own, and sealed for a type with a known set of variants.
9.2. Can an Abstract Class Be Sealed?
Yes. A sealed class is often abstract, like Pass in section 2, because the base type only defines the shared contract and the permitted subclasses do the work.
9.3. Are sealed and permits Reserved Keywords?
No. They are contextual keywords, so old code that uses sealed or permits as a variable or method name still compiles. The word non-sealed is the first Java keyword with a hyphen.
9.4. Do Sealed Classes Replace Enums?
No. An enum has a fixed set of instances, and each constant has the same fields. A sealed type has a fixed set of classes, and each class can have its own fields and any number of instances.
10. Conclusion
A sealed class or interface names its permitted subtypes, and every permitted subtype declares itself final, sealed or non-sealed. The subtypes must be in the same module, or the same package on the classpath, and the permits clause is optional when all of them are in one file.
Sealed interfaces with records model fixed sets of cases, and a switch over them needs no default, so the compiler points to every place that must change when a case is added. That compile-time check is the reason to use sealed types in object-oriented designs where the variants are known up front.
11. References
- JLS 25, sealed, non-sealed and final Classes
- JLS 25, sealed and non-sealed Interfaces
- JEP 409, Sealed Classes
- Class.getPermittedSubclasses() Javadoc
Happy Learning !!