Definition
- Same-file inference – if all permitted direct subtypes are declared in the same source file as the sealed type, the
permitsclause can be omitted. - Compiler inference – the compiler scans the file and infers the permitted set as exactly the subtypes that directly extend or implement the sealed type.
- All-or-nothing – if any subtype is in a different file, the
permitsclause is mandatory, listing every subtype. - Class-file identical – the inferred set is recorded in the
PermittedSubclassesattribute exactly as if written explicitly. - Modifier requirement – each permitted subtype must still carry
final,sealed, ornon-sealed; records are implicitly final.
Imagine National Bank of India writes its account policy on a single page. If every approved product is also described on that same page, head office does not need a separate approved list at the top. But if some products are on other pages, then head office must write the full list so readers know the complete set without hunting.
Understanding the Concepts
When is the permits clause optional?
The permits clause may be omitted when every permitted direct subtype is declared in the same source file (the same compilation unit) as the sealed type. The compiler infers the permitted set automatically.
package com.bank.model;
import java.math.BigDecimal;
// All subtypes in the same file: permits clause omitted
public sealed interface TransactionResult {
// Compiler infers: permits Approved, Declined, Pending
}
record Approved(BigDecimal amount) implements TransactionResult {}
record Declined(String reason) implements TransactionResult {}
record Pending(String referenceId) implements TransactionResult {}
This compiles exactly as if you had written permits Approved, Declined, Pending.
How does the compiler infer the permitted set?
The compiler reads the entire source file and finds all types that directly extend or implement the sealed type. These become the inferred permitted set. Adding a fourth subtype in the same file automatically extends the set.
package com.bank.model;
import java.math.BigDecimal;
// Adding Reversed here automatically includes it in the permitted set
public sealed interface TransactionResult {}
record Approved(BigDecimal amount) implements TransactionResult {}
record Declined(String reason) implements TransactionResult {}
record Pending(String referenceId) implements TransactionResult {}
record Reversed(String txnId, String reason) implements TransactionResult {}
Does inference work for nested types?
Yes. Inference covers both auxiliary top-level classes in the file and nested types.
package com.bank.model;
import java.math.BigDecimal;
public sealed interface Account {
// Nested records: compiler infers permits
record SavingsAccount(String id, BigDecimal balance)
implements Account {}
record CurrentAccount(String id, BigDecimal balance)
implements Account {}
}
When does permits become mandatory?
The moment any permitted subtype is in a different file, the permits clause is required. You cannot mix inference and explicit listing.
// File: TransactionResult.java
package com.bank.model;
// Approved is in another file, so permits is MANDATORY
public sealed interface TransactionResult
permits Approved, Declined, Pending {}
record Declined(String reason) implements TransactionResult {}
record Pending(String referenceId) implements TransactionResult {}
// File: Approved.java
package com.bank.model;
import java.math.BigDecimal;
public record Approved(BigDecimal amount) implements TransactionResult {}
What about the final, sealed, or non-sealed requirement?
Each permitted subtype must carry one of three modifiers regardless of whether permits is written:
final– cannot be further subclassed (records are implicitly final).sealed– can be extended, but only by its own permitted set.non-sealed– reopens the hierarchy below this type.
package com.bank.model;
// All in same file, permits omitted
public sealed interface Account {}
final class SavingsAccount implements Account {
// final: no further subclasses
}
non-sealed class CurrentAccount implements Account {
// non-sealed: anyone can extend CurrentAccount
}
Is the bytecode the same with and without permits?
Yes. The inferred permitted set is recorded in the PermittedSubclasses class-file attribute identically to an explicit permits clause. At the bytecode level there is no difference. The sealing guarantee is identical.
# Inspect the class file attribute
javap -v com.bank.model.TransactionResult
# Output includes:
# PermittedSubclasses:
# com/bank/model/Approved
# com/bank/model/Declined
# com/bank/model/Pending
How does exhaustive switch work with inferred permits?
Exhaustive switch works exactly the same. The compiler uses the inferred permitted set for exhaustiveness checking.
package com.bank.service;
import com.bank.model.*;
public class ResultHandler {
public static String handle(TransactionResult result) {
return switch (result) {
case Approved a -> "Approved: " + a.amount();
case Declined d -> "Declined: " + d.reason();
case Pending p -> "Pending: " + p.referenceId();
// No default: exhaustive even with inferred permits
};
}
}
What should a senior engineer know?
Same-file inference is a source-level convenience for small algebraic-data-type-style models. It does not weaken the sealing guarantee. For large hierarchies split across files for readability, use explicit permits. The key benefit is that adding a new subtype in the same file automatically includes it in the permitted set and causes every non-exhaustive switch to fail compilation, keeping the safety guarantee intact.
Visual Overview
Same-file inference:
TransactionResult.java
┌──────────────────────────────────────┐
│ sealed interface TransactionResult │ (no permits clause)
│ │
│ record Approved(...) ──▶ inferred │
│ record Declined(...) ──▶ inferred │
│ record Pending(...) ──▶ inferred │
└──────────────────────────────────────┘
│
▼ class file
PermittedSubclasses: Approved, Declined, Pending
(identical to explicit permits)
Different-file rule:
TransactionResult.java Approved.java (separate file)
┌──────────────────┐ ┌───────────────┐
│ sealed interface │ │ record Approved│
│ permits Approved, │◀────│ implements ... │
│ Declined, Pending │ └───────────────┘
└──────────────────┘
permits clause MANDATORY
Key Takeaway
The permits clause is optional when all permitted subtypes are declared in the same source file. The compiler infers the permitted set by scanning the file for all types that directly extend or implement the sealed type. This is a genuine convenience for small, closed hierarchies written together in one file.
The inference is all-or-nothing. The moment any subtype moves to a different file, permits becomes mandatory and must list every subtype. Adding a new subtype in the same file automatically extends the inferred set and causes every non-exhaustive switch to fail compilation.
At National Bank of India, Rajesh modeled transaction results as a sealed interface with three record subtypes, all in one file, and skipped the permits clause. The compiler inferred the set, the exhaustive switch worked with no default, and the PermittedSubclasses attribute in the class file was identical to an explicit declaration. The sealing guarantee is exactly the same.
The key point for seniors is that inference is purely a source-level convenience. It does not weaken the closed-hierarchy safety. Each permitted subtype still needs final, sealed, or non-sealed (records are implicitly final). For large hierarchies across multiple files, use explicit permits.
Remember one sentence: the permits clause is optional when all subtypes are in the same file because the compiler infers them, and the sealing guarantee is identical.
---