Explain how 'singleton constants' for enums versus 'closed set of subtypes' for sealed types affects instance count, identity, and exhaustiveness guarantees the compiler can give.
answer
- enum entry = one shared instance (=== ==)
- sealed data class = many instances, structural equality
- EnumMap/EnumSet cheap via ordinal
- exhaustiveness: enum lists entries, sealed lists subtypes (module-closed)
- add a case => compile error in exhaustive when
basics
~20 sEnum entries are single shared instances, so you can compare them by identity and use them as map keys cheaply. Sealed subtypes can have unlimited instances. Both let the compiler verify a when covers every case, but it counts entries for enums and subtypes for sealed.
solid answer
~50 sAn enum's entries are **singletons**: there is one `Direction.NORTH` for the whole JVM, so referential and structural equality coincide, `===` works as an identity check, and they're ideal keys for `EnumMap`/`EnumSet` (array-backed, very fast). Their finite, fixed count is what lets the compiler prove a `when` is exhaustive by enumerating entries. A sealed type instead closes the **set of subtypes**, not the number of instances: each subtype is a normal class, so you may create unlimited instances (`Node(1)`, `Node(2)`, ...). The compiler's exhaustiveness check counts the *known direct subtypes* — guaranteed because `sealed` restricts subclassing to the same module (same package for sealed classes), so it can see them all. For data-class cases, equality is value-based (from `data`), not identity. Practically: enum => identity + cheap keys + ordinal; sealed => structural per-case data + many instances; both => compiler-checked exhaustive `when` with no `else`, and adding a case turns missed branches into compile errors.
code
kotlin · 8 linesenum class Suit { HEARTS, SPADES }
println(Suit.HEARTS === Suit.HEARTS) // true: singleton
sealed interface Expr
data class Num(val v: Int) : Expr
val a = Num(1); val b = Num(1)
println(a == b) // true (structural / data class)
println(a === b) // false (different instances)go deeper
Knows enum entries are singletons and that when can be exhaustive over both, without the equality nuances.
Distinguishes singleton enum entries from multi-instance sealed data classes and explains structural vs identity equality.
Explains why the compiler can prove exhaustiveness (module/package closure), the ordinal-backed EnumMap/EnumSet performance, and when === is and isn't valid.
Connects instance/identity semantics to performance and API design (key choice, recursive ADTs, refactor safety) and reasons about exhaustiveness as a compile-time invariant across module boundaries.
## Instance count ### Enum: fixed singletons Every enum entry is a single shared instance created once when the enum class loads. `Suit.HEARTS` is the *same object* everywhere. You can never have two `Suit.HEARTS`. This guarantees: - **Identity equality:** `a === b` is true iff they're the same entry; `==` and `===` agree. - **Cheap collections:** `EnumSet`/`EnumMap` use the entries' `ordinal` to back themselves with bit-vectors/arrays — far faster and smaller than hash-based sets/maps. - **Stable `ordinal`/`name`:** each entry has a fixed position and name. ### Sealed: closed subtypes, open instances `sealed` closes the *set of direct subtypes*, not the instance count. A sealed subtype that's an `object` is a singleton (like an enum entry); a subtype that's a `class`/`data class` can be instantiated **any number of times**: ```kotlin sealed interface Expr data class Num(val v: Int) : Expr // many: Num(1), Num(2), ... data class Add(val l: Expr, val r: Expr) : Expr data object Zero : Expr // one instance ``` So `Num(1)` and `Num(2)` are distinct objects; equality is **structural** (data class), not identity. ## Identity and equality - Enum: identity == structural (singletons). Safe to use `===`. - Sealed `object`/`data object` cases: singletons too, so identity works. - Sealed `data class` cases: equality is value-based; two equal-valued instances are `==` but not `===`. Never rely on `===` for these. ## Exhaustiveness — why the compiler can prove it A `when` used as an **expression** must be exhaustive (and since Kotlin 1.7, exhaustiveness is also enforced for `when` **statements** over sealed/enum subjects). The compiler proves coverage because the candidate set is *closed and known*: - **Enum:** it knows the full list of entries. - **Sealed:** `sealed` forbids subtypes outside the same module (same package for sealed classes), so the compiler can see *all* direct subtypes and require a branch for each. ```kotlin fun eval(e: Expr): Int = when (e) { // no else needed is Num -> e.v is Add -> eval(e.l) + eval(e.r) Zero -> 0 } ``` Add a new subtype `Mul` and this `when` stops compiling until you handle it — the compiler turns 'I forgot a case' into a build error. The same happens when you add an enum entry to an exhaustive `when`. ## Why it matters in design - Need identity, ordinal ordering, fast set/map keys, name-based serialization => the singleton nature of **enums** is a feature. - Need many instances each carrying different data, recursive structures (ASTs), or per-case payloads => **sealed**, accepting structural equality and no built-in ordinal. - Both give the safety net of compiler-checked exhaustiveness; that's the shared reason to model closed sets with either. ## Pitfall Don't assume `===` works on sealed data-class cases — it compares references, so two structurally-equal `Num(1)` values are not `===`. And don't expect a sealed subtype to be a singleton just because the enum it replaced was — only `object`/`data object` cases are.
- Why are EnumMap/EnumSet faster than HashMap/HashSet of enums?They exploit the fixed ordinal of singleton entries to use array/bit-vector storage, avoiding hashing and boxing.
- What lets the compiler prove a sealed when is exhaustive?sealed restricts subtypes to the same module (same package for classes), so the compiler sees every direct subtype and can require a branch for each.
- Is === safe on sealed cases?Only for object/data object singleton cases. data class cases use structural equality; === compares references and will differ for equal values.
saying these in an interview costs you the question
- Saying sealed subtypes are singletons like enum entries (only object cases are)
- Using === to compare data-class sealed cases
- Thinking exhaustiveness needs an else for sealed/enum
- Claiming the compiler can't verify sealed exhaustiveness without runtime info
- Confusing 'closed set of subtypes' with 'fixed number of instances'