How are sealed classes compiled on the JVM, and what are the rules around their constructors and instantiation?
answer
- Sealed class -> abstract JVM class
- Constructors protected by default (or private)
- KClass.sealedSubclasses lists direct subtypes
- Guarantee is compiler/metadata-enforced
- Java 17 PermittedSubclasses attribute optional mirror
basics
~20 sA sealed class compiles to an abstract class whose constructors are not accessible from outside, so it can't be subclassed or instantiated externally. Reflection can list its subclasses via sealedSubclasses, and the compiler tracks the permitted set.
solid answer
~40 sOn the JVM a `sealed class` becomes an `abstract` class; its constructors are effectively `protected`/package-private so foreign code can't subclass it. The compiler also records the permitted subtypes, and Kotlin reflection exposes them through `KClass.sealedSubclasses`. Because the parent is abstract, you instantiate only subtypes; the parent's constructor runs as part of subtype construction (super call). On modern JVM bytecode (Java 17+ targets) Kotlin can emit a JVM `permitted subclasses` attribute mirroring Java's `sealed` feature, but Kotlin's guarantee is primarily compiler-enforced, not solely dependent on the bytecode flag. Constructors of a sealed class are `protected` by default, and you can declare them `private`. A sealed *interface* has no constructor at all. Objects and data classes are common subtypes; each is a normal JVM type.
code
kotlin · 6 linessealed class Expr private constructor() { // private ctor allowed
data class Lit(val n: Int) : Expr()
data class Add(val l: Expr, val r: Expr) : Expr()
}
val kinds = Expr::class.sealedSubclasses // [Lit, Add]go deeper
May know it 'becomes abstract' but not the constructor or reflection details.
Knows it compiles to an abstract class and that subtypes are instantiated instead.
Explains protected constructors, sealedSubclasses (direct only), and that the guarantee is metadata/compiler-based.
Reasons about Java 17 interop, bytecode attributes, reflection-based registries, and the cross-language compatibility surface.
## Bytecode shape - A `sealed class` is compiled to a JVM **`abstract` class**. Being abstract is what forbids direct instantiation. - Its **constructors are `protected` by default** (you may declare them `private`). External, non-permitted code therefore cannot call them, which — together with the compiler's permitted-subtype check — prevents outside subclassing. - A `sealed interface` compiles to a normal JVM interface; it has **no constructor**. ## Permitted-subclass tracking The Kotlin compiler stores the closed set in metadata. Two things consume it: - **The compiler itself**, to enforce that only same-module/same-package subtypes exist and to drive exhaustiveness checks. - **Reflection:** `KClass.sealedSubclasses` returns the list of direct subclasses. ```kotlin sealed class Token { object Plus : Token() data class Number(val value: Int) : Token() } fun main() { // [class Token$Plus, class Token$Number] println(Token::class.sealedSubclasses) } ``` ## Interaction with Java's sealed feature Java 17 introduced its own `sealed`/`permits`. When targeting modern bytecode, Kotlin can emit the JVM **`PermittedSubclasses`** attribute so the hierarchy is honored at the bytecode level too. But Kotlin's closed-set guarantee is fundamentally **compiler-enforced via metadata**, so it works regardless of JVM target. Don't claim the guarantee 'only exists' because of the Java 17 attribute. ## Constructor rules summary - Sealed class constructors: `protected` by default; may be `private`; never `public` in effect to outsiders. - Cannot instantiate the sealed class — it's abstract. - Subtype construction calls the sealed parent's constructor via the normal `super(...)` mechanism, running any `init` block and initializing shared state. - Sealed interface: no constructor; subtypes implement it. ## Practical implications - You can safely rely on `sealedSubclasses` for things like registries, but note it returns *direct* subclasses only and requires the `kotlin-reflect` dependency. - `object` subtypes are singletons; `data class` subtypes get the usual generated members. Both are ordinary JVM classes nested or top-level.
- Does sealedSubclasses return indirect subclasses too?No — only direct subclasses. You'd have to recurse manually for deeper levels, and it needs kotlin-reflect.
- Is the closed-set guarantee lost if you target old JVM bytecode without PermittedSubclasses?No. Kotlin enforces it through the compiler and metadata; the JVM attribute is an optional mirror for Java interop, not the source of the guarantee.
saying these in an interview costs you the question
- Saying a sealed class compiles to a final class
- Claiming the guarantee depends entirely on Java 17 bytecode
- Stating sealedSubclasses returns the full transitive tree
- Thinking sealed class constructors are public