skip to content

What are the location/accessibility constraints on a sealed type and its permitted subtypes (same module/package, etc.), and why do they exist?

level: seniorimportance: should knowfreq 52%

answer

  1. Compiler must see the whole set at compile time
  2. Named module -> same module (any package within)
  3. Classpath / unnamed module -> same package
  4. Must directly extend and be accessible
  5. Omit permits -> same source file

basics

~20 s

A sealed type and all its permitted subtypes must live together: in a named module, in the same module; otherwise in the same package. Each permitted subtype must directly extend the sealed type and be accessible to it at compile time.

solid answer

~50 s

Sealing only works if the compiler can actually see and verify the whole closed set, so Java requires the sealed type and every permitted subtype to be co-located. If the sealed type is in a named module, all its permitted subtypes must be in that same module (they may be in different packages within it). If it is in the unnamed module (typical classpath code), all permitted subtypes must be in the same package. Additionally, each permitted subtype must directly extend or implement the sealed type, must be accessible from it (so the compiler can read both during compilation), and must be present at compile time. These rules guarantee the permitted set is fully knowable when the sealed type is compiled - which is exactly what makes exhaustiveness and the closed-set guarantee sound. They also mean you cannot let an external module or arbitrary third-party package inject a permitted subtype.

go deeper

for a junior

Knows that the sealed type and its permitted subtypes must live close together (same package, or same file when permits is omitted).

for a middle

Distinguishes the same-package (classpath) rule from the same-module rule, and knows subtypes must directly extend and be accessible.

for a senior

Explains why co-location is required (compile-time visibility of the full set for soundness) and applies it correctly when laying out packages/modules.

for a principal

Designs module/package boundaries around sealed contracts deliberately - deciding what stays closed within a module versus what is exposed via non-sealed as a supported extension point across module lines.

## The core constraint A sealed hierarchy is only meaningful if the compiler can **see and verify the entire permitted set at compile time**. To guarantee that, Java imposes **co-location** rules on a sealed type and the subtypes it permits. ### Module / package rule - **Named module:** if the sealed class/interface belongs to a **named module** (declared via `module-info.java`), then **all permitted subtypes must belong to the same module.** They may be spread across **different packages** within that module - the module is the boundary. - **Unnamed module (classpath):** if the sealed type is in the **unnamed module** (ordinary classpath code, no module system), then **all permitted subtypes must be in the same package** as the sealed type. The package is the boundary. > Terms: A **module** is the JPMS (Java Platform Module System, since Java 9) unit declared by `module-info.java`. The **unnamed module** is the implicit module that classpath code lives in when no modules are declared. A **package** is the usual `package com.foo;` grouping. ### Other required conditions For each type `S` named in `permits` of sealed type `T`: 1. **`S` must directly extend/implement `T`.** No skipping levels - a grand-subtype is not a permitted subtype of `T`. 2. **`S` must be accessible from `T`** at compile time (visibility allows `T`'s compilation unit to reference `S`). 3. **`S` must exist at compile time** - it cannot be generated later or supplied by an external artifact. 4. **The relationship must be mutual and explicit:** `T` lists `S` in `permits`, and `S` declares `extends T` / `implements T` and chooses `final` / `sealed` / `non-sealed`. ### If you omit `permits` When `permits` is omitted, the permitted subtypes are **inferred from the same compilation unit (source file)**. That is an even stricter co-location (same file), and is convenient for small hierarchies. ## Why these rules exist 1. **Soundness of the closed set.** Exhaustiveness in `switch` and the 'these are all the subtypes' guarantee are only safe if the compiler can read every member of the set **while compiling the sealed type**. Co-location ensures the whole set is available to one compiler invocation/module graph. 2. **No external injection.** A different module or arbitrary external package cannot sneak in a new permitted subtype. The closed set is a **local, controlled** contract, not something outsiders can extend (unless you opt a branch out with `non-sealed`). 3. **Reliable evolution boundary.** Because the set is bounded by a module/package you control, adding or removing a subtype is a deliberate, local edit - and any downstream exhaustive `switch` you also control will fail to compile until updated. ## Practical implications - You **cannot** split a sealed interface in one module and its implementations in a *consumer* module - put them in the same module (or expose `non-sealed` as the extension point). - On the classpath (no modules), keep the sealed type and all subtypes in **one package** - a frequent gotcha when refactoring packages. - `permits` may reference subtypes in **sibling packages of the same module**; module, not package, is the boundary under JPMS.

  • Can a permitted subtype live in a different package from the sealed type?
    Yes, but only if both are in the same named module. On the classpath (unnamed module) they must share the same package.
  • Why can't an external module supply a permitted subtype?
    Because the sealed type's permitted set must be fully resolvable when it is compiled; allowing external injection would break the closed-set/exhaustiveness guarantee. Use non-sealed to deliberately offer an open extension point instead.

saying these in an interview costs you the question

  • Claiming permitted subtypes can be in any package as long as they're listed - on the classpath they must share the package
  • Saying sealing works across separate modules for consumers - the permitted set must be within the sealed type's own module
  • Forgetting that a permitted subtype must DIRECTLY extend the sealed type, not via an intermediate
  • Assuming permits can name a type the sealed type can't access

context