When would you prefer per-constant enum bodies over a sealed class hierarchy, and how do per-constant abstract property overrides work?
answer
- enum = same-shape named singletons + shared op
- sealed = different data shapes / multiplicity / generics
- abstract val + per-constant override = property behavior
- get() defers; = stores
- init order: constants top-down, watch companion refs
basics
~20 sUse per-constant enum bodies for a fixed, small set of singletons that share one operation. Use sealed classes when variants carry different data shapes or need multiple instances. Property overrides use an abstract val with a per-constant getter.
solid answer
~40 sPer-constant enum bodies fit a **closed, finite set of stateless singletons** that differ only in behavior for a shared abstract member — the type-safe strategy pattern. Each constant is one instance, gets `name`/`ordinal`/`valueOf`/`entries` for free, and is serializable by name. Prefer a **sealed class/interface** when variants need **distinct data shapes**, multiple instances, generics, or richer hierarchies — enums force a single shared constructor signature and one instance per variant. For per-constant *property* behavior you declare `abstract val x: T` and each constant overrides it with a custom getter: `CONST { override val x get() = ... }` or `CONST { override val x = ... }`. Watch initialization order: an `abstract val` overridden by a property initializer is fine, but referencing other not-yet-initialized members from a getter can surprise you.
code
kotlin · 11 linesenum class PayMethod {
CARD { override val fee get() = 0.029; override fun authorize() = TODO() },
WALLET { override val fee get() = 0.015; override fun authorize() = TODO() };
abstract val fee: Double
abstract fun authorize(): Boolean
}
// Prefer sealed when shapes diverge:
sealed interface Shape { fun area(): Double }
data class Circle(val r: Double) : Shape { override fun area() = Math.PI * r * r }
data class Rect(val w: Double, val h: Double) : Shape { override fun area() = w * h }go deeper
Knows enums give name/ordinal for free and can override a method per constant.
Can override properties per constant and roughly states when an enum fits.
Articulates the enum-vs-sealed tradeoff precisely and handles the initialization-order trap with get() vs initializer.
Sets team guidance: closed strategy families in enums, divergent/parameterized variants in sealed hierarchies, and migration paths between them.
## Per-constant enum body vs sealed hierarchy Both model a **closed set of subtypes**. Choose by what varies: | Need | Enum with per-constant bodies | Sealed class/interface | |------|------|------| | Fixed, named singletons | Ideal — one instance each | Possible but verbose | | Each variant has different fields/shape | Awkward — shared constructor only | Ideal — each subclass its own ctor | | Many instances of a variant | Impossible (one per constant) | Natural | | Generics per variant | No | Yes | | Free `name`/`ordinal`/`entries`/`valueOf` | Yes | No | | Name-based (de)serialization | Built-in | Manual | Rule of thumb: **same data shape + finite named singletons + shared operation = enum**; **different data shapes or multiplicity = sealed**. ## Per-constant property override You can make a *property* vary per constant by declaring it abstract and overriding it in each constant body: ```kotlin enum class Suit { HEARTS { override val color = Color.RED }, SPADES { override val color = Color.BLACK }; abstract val color: Color } ``` or with a custom getter: ```kotlin enum class Level { LOW { override val multiplier get() = 1.0 }, HIGH { override val multiplier get() = 2.5 }; abstract val multiplier: Double } ``` - `override val x = expr` gives a stored property in the synthetic subclass. - `override val x get() = expr` computes on each access (no backing field). - Both work; pick stored for constants, getter for derived values. ## Initialization-order caveat Enum constants are initialized **in declaration order**, top to bottom, before the companion object and other static state in some ordering rules. If a per-constant property *initializer* (not a getter) references a `companion object` value or another member that is not yet initialized, you can read `null`/zero. Using a `get()` defers evaluation to call time and sidesteps this. This is a classic senior trap with bodied enums. ## When per-constant bodies become a smell - Bodies grow large -> the enum file becomes a dumping ground; extract to a sealed hierarchy or inject strategies. - You need per-instance configuration beyond constants -> not an enum's job. - Cross-cutting logic duplicated across constants -> add a shared concrete/`open` member and override selectively. ## Keywords in play `enum class`, `abstract val`, custom getter `get()`, `override`, `sealed class`/`sealed interface`, `entries`, `valueOf`.
- Why might `override val x = computeFromCompanion()` in a constant return an unexpected value?Constant initializers run in declaration order before some other static state; referencing a not-yet-initialized companion value can yield defaults. Use a get() to defer.
- Give one capability sealed classes have that bodied enums cannot match.Each sealed subclass can have its own constructor/fields and multiple instances; enum constants share one constructor and are singletons.
Enum bodies are pre-stamped coins of one mold differing by engraving; sealed classes are bespoke objects each shaped differently.
saying these in an interview costs you the question
- Reaching for an enum when variants have different data shapes
- Ignoring constant initialization order with companion references
- Claiming sealed and enum are interchangeable in all cases
- Not knowing properties can be overridden per constant
- Stuffing huge multi-method bodies into every constant without considering a strategy/sealed refactor