Back to Advanced Topics
Q25Question 25 of 41

Is listing all permitted subclasses optional if they're in the same file?

Key Concepts
  • The permits clause is optional when all permitted subtypes are declared in the same source file as the sealed type.
  • The compiler infers the permitted set as exactly those subtypes declared in the file that directly extend or implement the sealed type.
  • Inference works for both auxiliary top-level classes and nested types within the same file.
  • The moment any permitted subtype is in a different file, the permits clause becomes mandatory for all subtypes.
  • The inferred set is recorded in the PermittedSubclasses class-file attribute identically to an explicit permits clause.
  • Each permitted subtype must still be declared final, sealed, or non-sealed regardless of whether permits is written.

Definition

The `permits` clause is optional only when all permitted subtypes are in the same source file, where the compiler infers them automatically.
  • Same-file inference – if all permitted direct subtypes are declared in the same source file as the sealed type, the permits clause 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 permits clause is mandatory, listing every subtype.
  • Class-file identical – the inferred set is recorded in the PermittedSubclasses attribute exactly as if written explicitly.
  • Modifier requirement – each permitted subtype must still carry final, sealed, or non-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.

JAVA
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.

JAVA
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.

JAVA
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.

JAVA
// 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 {}
JAVA
// 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.
JAVA
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.

BASH
# 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.

JAVA
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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate