Definition
- Mandatory modifier – every direct permitted subclass must carry exactly one of
final,sealed, ornon-sealed. - Compile error – omitting all three produces "sealed, non-sealed or final modifiers expected."
final– ends the hierarchy at that subtype; no further extension.sealed– continues the closure with its ownpermitslist.non-sealed– deliberately reopens that branch for unrestricted extension.- Records and enums – implicitly
final, so they satisfy the rule without an extra modifier. - Interfaces – a permitted sub-interface must be
sealedornon-sealedbecause interfaces cannot befinal.
Imagine National Bank of India approves a product to be a kind of Account. Head office insists that each approved product must stamp its own policy on further variations: either "no further variants allowed" (final), "only my own named variants allowed" (sealed), or "anyone may add variants freely" (non-sealed). A product that forgets to stamp any policy is sent back: the form is incomplete, and head office refuses to file it.
Understanding the Concepts
What error do you get without the modifier?
The compiler emits "sealed, non-sealed or final modifiers expected" when a permitted subclass omits all three modifiers.
package com.bank.model;
import java.math.BigDecimal;
public sealed interface Account permits SavingsAccount {
BigDecimal balance();
}
// COMPILE ERROR: sealed, non-sealed or final modifiers expected
// class SavingsAccount implements Account {
// public BigDecimal balance() { return new BigDecimal("500000"); }
// }
// CORRECT: add final
final class SavingsAccount implements Account {
@Override
public BigDecimal balance() {
return new BigDecimal("500000");
}
}
Why does Java require this modifier?
Sealing must not leak. If a permitted subtype could be extended freely without saying so, the hierarchy would no longer be closed because unknown classes could slip in beneath it.
package com.bank.model;
import java.math.BigDecimal;
// Without the modifier rule, this would leak the sealed hierarchy:
// sealed interface Account permits SavingsAccount { ... }
// class SavingsAccount implements Account { ... } // no modifier
// class RogueSavings extends SavingsAccount { } // anyone could sneak in!
// With the rule: SavingsAccount must declare final, sealed, or non-sealed
sealed interface Account permits SavingsAccount, LoanAccount {
BigDecimal balance();
}
// final: no one can extend this
final class SavingsAccount implements Account {
@Override
public BigDecimal balance() { return new BigDecimal("500000"); }
}
// sealed: continues the restriction
sealed class LoanAccount implements Account permits HomeLoan {
@Override
public BigDecimal balance() { return new BigDecimal("1000000"); }
}
final class HomeLoan extends LoanAccount {}
What does each modifier mean?
| Modifier | Meaning | Extension Rule |
|---|---|---|
| final | Branch ends here | No class may extend this |
| sealed | Branch continues closed | Only its own permitted subtypes |
| non-sealed | Branch reopens | Any class may extend freely |
package com.bank.model;
import java.math.BigDecimal;
public sealed interface Account
permits SavingsAccount, LoanAccount, PartnerProduct {
BigDecimal balance();
}
final class SavingsAccount implements Account {
@Override
public BigDecimal balance() { return new BigDecimal("500000"); }
}
sealed class LoanAccount implements Account permits HomeLoan, CarLoan {
@Override
public BigDecimal balance() { return new BigDecimal("1000000"); }
}
final class HomeLoan extends LoanAccount {}
final class CarLoan extends LoanAccount {}
non-sealed class PartnerProduct implements Account {
@Override
public BigDecimal balance() { return BigDecimal.ZERO; }
}
// Anyone can extend PartnerProduct
class WalletProduct extends PartnerProduct {}
How do records and enums satisfy the rule?
Records are implicitly final, so a record implementing a sealed interface satisfies the rule automatically. Enums are effectively final too.
package com.bank.model;
import java.math.BigDecimal;
public sealed interface PaymentResult {
// Records are implicitly final: no extra modifier needed
record Success(BigDecimal amount) implements PaymentResult {}
record Failure(String reason) implements PaymentResult {}
record Pending(String expectedDate) implements PaymentResult {}
}
package com.bank.service;
import com.bank.model.PaymentResult;
import com.bank.model.PaymentResult.*;
import java.math.BigDecimal;
public class PaymentDemo {
public static void main(String[] args) {
PaymentResult result = new Success(new BigDecimal("75000"));
String msg = switch (result) {
case Success s -> "Paid " + s.amount() + " INR";
case Failure f -> "Failed: " + f.reason();
case Pending p -> "Pending until " + p.expectedDate();
};
System.out.println(msg);
}
}
Paid 75000 INR
What about interfaces as permitted subtypes?
A permitted subtype that is itself an interface must be sealed or non-sealed. Interfaces cannot be final in Java.
package com.bank.model;
import java.math.BigDecimal;
public sealed interface Financial
permits Account, Investment {
BigDecimal value();
}
// Sub-interface: must be sealed or non-sealed (not final)
sealed interface Account extends Financial
permits SavingsAcc {
@Override BigDecimal value();
}
final class SavingsAcc implements Account {
@Override public BigDecimal value() { return new BigDecimal("500000"); }
}
non-sealed interface Investment extends Financial {
@Override BigDecimal value();
}
How does this affect exhaustiveness checking?
Because every subtype declares its extensibility, the compiler can compute the complete leaf set for exhaustiveness checking. A non-sealed branch is the one place where the set becomes open, and the compiler requires a default or wildcard when switching if the open branch is reachable.
package com.bank.service;
import com.bank.model.*;
import java.math.BigDecimal;
public class ExhaustiveDemo {
public String classify(Account account) {
return switch (account) {
case SavingsAccount s -> "Savings";
case LoanAccount l -> "Loan";
case PartnerProduct p -> "Partner"; // covers all partner sub-types
};
}
}
What are common mistakes?
Forgetting the modifier is the most frequent first-time error. The fix is to decide and declare it.
// WRONG: no modifier
// class SavingsAccount implements Account { ... }
// Error: sealed, non-sealed or final modifiers expected
// FIX: decide what kind of subtype this is
final class SavingsAccount implements Account {
@Override
public BigDecimal balance() { return new BigDecimal("500000"); }
}
What should a senior engineer know?
This requirement makes the closed-set guarantee hold transitively. The modifier forces an architectural decision at each product type about whether future variants are allowed, which is exactly the kind of deliberate design a regulated banking system benefits from. In code reviews at National Bank of India, Pankaj ensures every permitted subtype has an explicit extensibility stamp.
Visual Overview
┌─────────────────────────────────────────────┐ │ Permitted Subtype Modifier Requirement │ ├─────────────────────────────────────────────┤ │ │ │ sealed interface Account permits A, B, C │ │ │ │ │ ├── A: final │ │ │ └── Branch ends. No extension. │ │ │ │ │ ├── B: sealed permits D, E │ │ │ ├── D: final │ │ │ └── E: final │ │ │ └── Branch continues closed. │ │ │ │ │ └── C: non-sealed │ │ ├── F extends C (anyone) │ │ └── G extends C (anyone) │ │ └── Branch reopened. │ │ │ │ Missing modifier? │ │ ┌──────────────────────────────┐ │ │ │ COMPILE ERROR: │ │ │ │ "sealed, non-sealed or │ │ │ │ final modifiers expected" │ │ │ └──────────────────────────────┘ │ │ │ │ Records / Enums? │ │ └── Implicitly final → rule satisfied ✓ │ └─────────────────────────────────────────────┘
Key Takeaway
If a permitted subclass has no final, sealed, or non-sealed modifier, it is a compile error. The language requires every class or interface that directly extends a sealed type to declare its own extensibility, and leaving all three off produces "sealed, non-sealed or final modifiers expected."
The reason is that sealing must not leak. A sealed type closes its hierarchy at the first level by naming permitted subtypes, but if one of those subtypes could be extended freely without saying so, unknown classes could slip in and the hierarchy would no longer be closed. So each permitted subtype states its policy: final ends the branch, sealed continues the closure, and non-sealed deliberately reopens it.
Records and enums are implicitly final, so they satisfy the rule automatically. In a banking project at National Bank of India, Pankaj uses records for payment results: Success, Failure, and Pending all implement a sealed PaymentResult interface with no extra modifier needed. For ordinary classes and interfaces, the modifier must be written explicitly.
The value of this rule in production is that it turns extensibility into an explicit, reviewed architectural decision at each product type rather than an accident. The modifier forces the team to think about whether a subtype should be a closed leaf, a further-restricted branch, or an open extension point, which is exactly the deliberate design a regulated system needs.
Remember one sentence: every permitted subclass must be final, sealed, or non-sealed; omitting all three is a compile error that forces an explicit extensibility decision.
---