skip to content

When would you prefer per-constant enum bodies over a sealed class hierarchy, and how do per-constant abstract property overrides work?

level: seniorimportance: should knowfreq 28%

answer

  1. enum = same-shape named singletons + shared op
  2. sealed = different data shapes / multiplicity / generics
  3. abstract val + per-constant override = property behavior
  4. get() defers; = stores
  5. init order: constants top-down, watch companion refs

basics

~20 s

Use 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 s

Per-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 lines
kotlin
enum 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

for a junior

Knows enums give name/ordinal for free and can override a method per constant.

for a middle

Can override properties per constant and roughly states when an enum fits.

for a senior

Articulates the enum-vs-sealed tradeoff precisely and handles the initialization-order trap with get() vs initializer.

for a principal

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

context