A sealed class or interface restricts which types may directly extend or implement it through a declared set of permitted variants.
Java sealed types: restrict a domain’s direct variants
Java 17+. Use a JDK that supports this release.
Model a closed decision
A settlement is either accepted with an amount or rejected with a reason. Declaring that pair as permitted variants prevents unrelated implementations from entering this particular model without changing its type declaration.
Each permitted direct subtype must state its extension policy: final, sealed, or non-sealed. Records are final, which fits these leaf values. A non-sealed subtype deliberately reopens its branch; it is not another closed leaf.
Permitted types obey locality rules. In a named module they belong to the same module; in the unnamed module they belong to the same package. The sample keeps them together as nested types in one source file.
A closed hierarchy still needs validation
Knowing the set of variants does not prove an accepted amount is nonnegative or a rejection reason is useful. The record constructors validate those rules. Type closure and value validity solve different problems.
The Java 17 program uses instanceof with a binding variable. The Java 21 pattern switch lesson shows exhaustive branching without a catch-all fallback that could conceal a newly added variant.
Working program
public class SettlementVariants {
sealed interface Settlement permits Accepted, Rejected { }
record Accepted(long cents) implements Settlement {
Accepted { if (cents < 0) throw new IllegalArgumentException("Negative amount"); }
}
record Rejected(String reason) implements Settlement {
Rejected {
if (reason == null || reason.isBlank()) throw new IllegalArgumentException("Reason required");
}
}
static String describe(Settlement settlement) {
if (settlement instanceof Accepted accepted) return "accepted=" + accepted.cents();
if (settlement instanceof Rejected rejected) return "rejected=" + rejected.reason();
throw new IllegalArgumentException("Settlement required");
}
public static void main(String[] args) {
System.out.println(describe(new Accepted(1250)));
System.out.println(describe(new Rejected("expired")));
}
}Output
accepted=1250
rejected=expiredCost and design choices
The type test selects one of a small fixed number of variants. It does not require scanning a registry. The resulting text allocation depends on its length.
Use an open interface for plugin implementations that independent modules must add. Closing that interface and later adding reflection-based escape routes defeats the ownership boundary.
Common Mistakes
- Do not forget the extension policy of a permitted subtype.
- Do not use non-sealed while claiming the entire branch is closed.
- Do not confuse a permitted type with a validated value.
Connect the contracts
Compare the boundary explained in Open contracts with the assumptions made by this program.
