Java Enums
Purpose
Turn a closed set of values into a type the compiler and the runtime both understand, and
keep it safe to evolve. Two failure modes: the "enum" that is really an int or a String,
so nothing rejects an invalid value and every use site re-implements the mapping; and the
real enum whose identity has leaked into a database column, a wire format or an exhaustive
switch, so adding a constant becomes a migration and a coordinated deploy.
Workflow
Inspect compiler release/toolchains, runtime, persistence provider/spec, mapper/schema versions,
stored values and supported consumers first. No single authoring baseline is declared; Java 25
is referenced, while switch expressions require Java 14+, records Java 16+, and collection
copy factories Java 10+. @EnumeratedValue needs Persistence 3.2 support. Use the target's
existing alternatives; do not upgrade or enable preview. If consumer/mapping evidence is
missing, keep evolution claims conditional and state the checks needed before release.
- Confirm the set is closed for the compatibility horizon—statuses and error categories may
qualify; currencies and standards can evolve. If new values arrive independently from outside
the code (tenant-configured categories,
plugin-provided types), an enum is the wrong shape; use a value type with validation.
- Give each constant its data as instance fields, assigned through the constructor.
Anything derived from position — an id, a code, a weight, a display name — is a field, not
ordinal().
- Place varying behaviour with its owner. Constant-specific bodies or a strategy field fit
intrinsic behavior; an exhaustive caller-side switch fits a concern owned by that caller.
- Choose the collection by the type, not the habit.
EnumSet replaces bit fields;
EnumMap replaces arrays indexed by ordinal().
- Decide the external representation explicitly before the first release: an explicit
code field for storage and wire,
name() only when you accept that renaming a constant is
a breaking change.
- Plan for a consumer that does not know a constant yet. In a rolling deploy the producer
is ahead of the consumer for minutes to hours; decide now whether that is an error, a
fallback, or a rejected message.
Rules
- Prefer an enum when a set is closed for the deployment/compatibility horizon and values need
type-safe identity. It buys compile-time checking and a namespace;
toString() defaults to the
identifier and is not automatically a user-facing label. Independent boolean dimensions may
remain booleans or become EnumSet, not one mutually exclusive enum.
- Never derive meaning from
ordinal(). It changes when someone reorders or inserts a
constant — a source change that compiles cleanly and silently reinterprets existing data.
Declare an explicit field (code, id, weight) and a lookup map for the reverse
direction. ordinal() exists for EnumSet/EnumMap internals.
- Do not persist an ordinal as domain identity. In common JPA mappings,
@Enumerated defaults to
ORDINAL unless newer metadata such as @EnumeratedValue changes inference—declare the mapping
explicitly. STRING couples storage to name(); an AttributeConverter or explicit scalar
mapping can use stable codes. Verify provider/spec version, constraints and unknown-value policy.
- Prefer
EnumSet to bit fields and to HashSet for enum elements: it is a bit vector
internally, so it is compact and fast, and it prints and iterates in declaration order.
It is not thread-safe and it is mutable. A wrapper is a live unmodifiable view; copy then
wrap for a snapshot, including the empty ordinary-set case described in the patterns reference.
- Prefer
EnumMap to HashMap for enum keys and to any array indexed by ordinal(). It is
array-backed with declaration-order iteration, and it removes the manual index arithmetic
that breaks when a constant is inserted.
values() exposes an array callers can modify without changing enum constants. javac
commonly implements it by cloning a stored array; that lowering and allocation elimination
are implementation details. Cache privately only when profiling shows repeated calls matter,
and never expose a shared mutable cached array.
- Put intrinsic per-constant behaviour on the constant. Two forms, both valid: an abstract method with a
body per constant, or a field holding a shared strategy (the strategy enum — several
constants delegating to the same nested strategy enum) when constants group into a few
behaviours. A
switch (this) is not inherently unsafe: an exhaustive switch expression without
a catch-all gives compiler assistance; choose based on behavior ownership and extension cost.
- Extend an enum's reach with an interface, not with inheritance — enums cannot be extended.
Declare the interface, let several enums implement it, and program against the interface
(
<T extends Enum<T> & Operation> when the code needs both). This allows several closed enum
sets behind one contract; truly open plugin values may need ordinary classes/records and a registry.
- Use an exhaustive
switch expression without a catch-all for dispatch over an enum you own: the
compiler then fails source recompilation when a constant is added. Traditional statement
switches may fall through; enhanced exhaustive switches can synthesize a runtime failure for an
unforeseen constant. When the enum comes from another artifact, separate compilation means a
new constant can reach old bytecode, so test the exact switch form and deployment policy.
- An enum with a mutable static field is shared mutable state with a nicer name; the constants
are singletons for the whole class loader, reachable from every thread. Constants may hold
immutable data freely, a lazily built lookup map safely (build it in a static initialiser),
and mutable state only under the same discipline as any other shared object.
- A single-element enum provides serialization/class-initialization guarantees useful for some
process/class-loader singletons—see java-object-construction—and an
enum with an abstract method is a compact state machine, but neither should be used where
the set is genuinely open.
- Do not switch on an enum in a
hashCode, equals or compareTo implementation and expect
cross-JVM stability: Enum.hashCode is identity-based and differs per run, and
compareTo is defined by ordinal. Sorting by declaration order is legitimate inside a
process; persisting or transmitting anything derived from it is not.
References
Deliver the chosen set/representation, compatibility and unknown-value policy, and checks
executed against old readers and representative stored/wire values. For collection changes,
test empty input and alias mutation. Separate compiler checks, integration tests and measured
performance from assumptions; written deployment cases are not executed verification.
- Enum patterns — read when deciding between constant-specific
bodies, strategy enums and an interface; when replacing a
switch chain; or when an enum is
becoming a state machine or a registry.
- Enums across boundaries — read before an enum
reaches a database column, a JSON contract, a message schema or another team's code, and
whenever adding or removing a constant needs a deployment plan.
1---2name: java-enums3description: Enums as types rather than labelled integers: instance fields instead of ordinal, constant-specific behaviour and strategy enums, extensibility through interfaces, EnumSet and EnumMap instead of bit fields and ordinal-indexed arrays, exhaustive switch and what separate compilation does to it, and what happens when an enum value crosses a database, a JSON payload or a topic. Use when int or String constants stand in for a closed set, when ordinal() appears anywhere outside a library, when @Enumerated is declared ORDINAL or left at its default, when a switch over an enum has a default branch that hides new constants, when adding a constant breaks a consumer during a rolling deploy, when values() is called in a loop, or when a set of flags is packed into an int. Does not cover annotations (java-annotations), sealed hierarchies and records as the open-data alternative (java-composition-over-inheritance), or equality and ordering contracts in general (java-object-contracts).4---56# Java Enums78## Purpose910Turn a closed set of values into a type the compiler and the runtime both understand, and11keep it safe to evolve. Two failure modes: the "enum" that is really an `int` or a `String`,12so nothing rejects an invalid value and every use site re-implements the mapping; and the13real enum whose identity has leaked into a database column, a wire format or an exhaustive14switch, so adding a constant becomes a migration and a coordinated deploy.1516## Workflow1718Inspect compiler release/toolchains, runtime, persistence provider/spec, mapper/schema versions,19stored values and supported consumers first. No single authoring baseline is declared; Java 2520is referenced, while switch expressions require Java 14+, records Java 16+, and collection21copy factories Java 10+. `@EnumeratedValue` needs Persistence 3.2 support. Use the target's22existing alternatives; do not upgrade or enable preview. If consumer/mapping evidence is23missing, keep evolution claims conditional and state the checks needed before release.24251. **Confirm the set is closed for the compatibility horizon**—statuses and error categories may26 qualify; currencies and standards can evolve. If new values arrive independently from outside27 the code (tenant-configured categories,28 plugin-provided types), an enum is the wrong shape; use a value type with validation.292. **Give each constant its data as instance fields**, assigned through the constructor.30 Anything derived from position — an id, a code, a weight, a display name — is a field, not31 `ordinal()`.323. **Place varying behaviour with its owner.** Constant-specific bodies or a strategy field fit33 intrinsic behavior; an exhaustive caller-side switch fits a concern owned by that caller.344. **Choose the collection by the type, not the habit.** `EnumSet` replaces bit fields;35 `EnumMap` replaces arrays indexed by `ordinal()`.365. **Decide the external representation explicitly** before the first release: an explicit37 code field for storage and wire, `name()` only when you accept that renaming a constant is38 a breaking change.396. **Plan for a consumer that does not know a constant yet.** In a rolling deploy the producer40 is ahead of the consumer for minutes to hours; decide now whether that is an error, a41 fallback, or a rejected message.4243## Rules4445- Prefer an enum when a set is closed for the deployment/compatibility horizon and values need46 type-safe identity. It buys compile-time checking and a namespace; `toString()` defaults to the47 identifier and is not automatically a user-facing label. Independent boolean dimensions may48 remain booleans or become `EnumSet`, not one mutually exclusive enum.49- Never derive meaning from `ordinal()`. It changes when someone reorders or inserts a50 constant — a source change that compiles cleanly and silently reinterprets existing data.51 Declare an explicit field (`code`, `id`, `weight`) and a lookup map for the reverse52 direction. `ordinal()` exists for `EnumSet`/`EnumMap` internals.53- Do not persist an ordinal as domain identity. In common JPA mappings, `@Enumerated` defaults to54 `ORDINAL` unless newer metadata such as `@EnumeratedValue` changes inference—declare the mapping55 explicitly. `STRING` couples storage to `name()`; an `AttributeConverter` or explicit scalar56 mapping can use stable codes. Verify provider/spec version, constraints and unknown-value policy.57- Prefer `EnumSet` to bit fields and to `HashSet` for enum elements: it is a bit vector58 internally, so it is compact and fast, and it prints and iterates in declaration order.59 It is not thread-safe and it is mutable. A wrapper is a live unmodifiable view; copy then60 wrap for a snapshot, including the empty ordinary-set case described in the patterns reference.61- Prefer `EnumMap` to `HashMap` for enum keys and to any array indexed by `ordinal()`. It is62 array-backed with declaration-order iteration, and it removes the manual index arithmetic63 that breaks when a constant is inserted.64- `values()` exposes an array callers can modify without changing enum constants. javac65 commonly implements it by cloning a stored array; that lowering and allocation elimination66 are implementation details. Cache privately only when profiling shows repeated calls matter,67 and never expose a shared mutable cached array.68- Put intrinsic per-constant behaviour on the constant. Two forms, both valid: an abstract method with a69 body per constant, or a field holding a shared strategy (the _strategy enum_ — several70 constants delegating to the same nested strategy enum) when constants group into a few71 behaviours. A `switch (this)` is not inherently unsafe: an exhaustive switch expression without72 a catch-all gives compiler assistance; choose based on behavior ownership and extension cost.73- Extend an enum's reach with an interface, not with inheritance — enums cannot be extended.74 Declare the interface, let several enums implement it, and program against the interface75 (`<T extends Enum<T> & Operation>` when the code needs both). This allows several closed enum76 sets behind one contract; truly open plugin values may need ordinary classes/records and a registry.77- Use an exhaustive `switch` expression without a catch-all for dispatch over an enum you own: the78 compiler then fails source recompilation when a constant is added. Traditional statement79 switches may fall through; enhanced exhaustive switches can synthesize a runtime failure for an80 unforeseen constant. When the enum comes from another artifact, separate compilation means a81 new constant can reach old bytecode, so test the exact switch form and deployment policy.82- An enum with a mutable static field is shared mutable state with a nicer name; the constants83 are singletons for the whole class loader, reachable from every thread. Constants may hold84 immutable data freely, a lazily built lookup map safely (build it in a static initialiser),85 and mutable state only under the same discipline as any other shared object.86- A single-element enum provides serialization/class-initialization guarantees useful for some87 process/class-loader singletons—see java-object-construction—and an88 enum with an abstract method is a compact state machine, but neither should be used where89 the set is genuinely open.90- Do not switch on an enum in a `hashCode`, `equals` or `compareTo` implementation and expect91 cross-JVM stability: `Enum.hashCode` is identity-based and differs per run, and92 `compareTo` is defined by ordinal. Sorting by declaration order is legitimate _inside_ a93 process; persisting or transmitting anything derived from it is not.9495## References9697Deliver the chosen set/representation, compatibility and unknown-value policy, and checks98executed against old readers and representative stored/wire values. For collection changes,99test empty input and alias mutation. Separate compiler checks, integration tests and measured100performance from assumptions; written deployment cases are not executed verification.101102- [Enum patterns](references/enum-patterns.md) — read when deciding between constant-specific103 bodies, strategy enums and an interface; when replacing a `switch` chain; or when an enum is104 becoming a state machine or a registry.105- [Enums across boundaries](references/enums-across-boundaries.md) — read before an enum106 reaches a database column, a JSON contract, a message schema or another team's code, and107 whenever adding or removing a constant needs a deployment plan.