Enums in Java: From Basics to EnumSet and EnumMap
Executive Summary
Enums in Java declare a fixed set of named instances, and the JVM guarantees each is a singleton, so == comparison is safe and each constant is a real object. Enums can define fields, constructors, and methods, including abstract methods with a body per constant, which makes them small behavior carriers. Switch expressions over enums enforce exhaustiveness: add a constant and every switch that misses it fails to compile, which is how state machines police themselves. EnumSet stores sets of constants in a bit vector and EnumMap keys values by ordinal in a plain array, making both faster and smaller than their hash-based equivalents for enum-typed data. Persist enum values by name(), never by ordinal(). For variant sets that grow at runtime, sealed interfaces, covered next, are the honest tool. For closed sets, enums are the last tool you need.
Enum Basics: The Syntax and the Guarantee
An enum is declared with the enum keyword, and its constants are listed up front, as the enum lesson shows:
enum OrderStatus {
NEW, PAID, SHIPPED, DELIVERED, CANCELLED
}
In practice, that declaration compiles to a class with five instances, exactly one per constant, and no way to create more. Consequently, three guarantees follow. Reference equality works: status == OrderStatus.PAID is safe because there is only one PAID object in the JVM. The name and position are available: status.name() returns “PAID”, status.ordinal() returns 1. And the full set is iterable: OrderStatus.values() returns the constants in declaration order, while OrderStatus.valueOf(“PAID”) parses text back to a constant (throwing IllegalArgumentException for garbage).
for (OrderStatus status : OrderStatus.values()) {
System.out.println(status.ordinal() + ": " + status.name());
}
// 0: NEW
// 1: PAID
// 2: SHIPPED
// ...
Constants with Behavior: Fields, Constructors, and Bodies
Also, each constant can carry data and behavior. Constructors (implicitly private) run once per constant, fields hold per-constant data. Also, an abstract method forces every constant to supply its own body. Here is the arithmetic example the JDK tutorial made famous, plus a state machine in the same shape:
enum Operation {
PLUS { public int apply(int a, int b) { return a + b; } },
MINUS { public int apply(int a, int b) { return a - b; } },
TIMES { public int apply(int a, int b) { return a * b; } };
public abstract int apply(int a, int b); // every constant must implement
}
enum OrderStatus {
NEW, PAID, SHIPPED, DELIVERED, CANCELLED;
public boolean canTransitionTo(OrderStatus next) {
return switch (this) { // exhaustive: no default needed
case NEW -> next == PAID || next == CANCELLED;
case PAID -> next == SHIPPED || next == CANCELLED;
case SHIPPED -> next == DELIVERED;
case DELIVERED, CANCELLED -> false;
};
}
}
public class EnumDemo {
public static void main(String[] args) {
System.out.println(Operation.PLUS.apply(3, 4)); // 7
OrderStatus order = OrderStatus.NEW;
System.out.println(order.canTransitionTo(OrderStatus.PAID)); // true
System.out.println(order.canTransitionTo(OrderStatus.DELIVERED)); // false
}
}
The Operation enum shows per-constant bodies: PLUS, MINUS, and TIMES each implement apply differently. The OrderStatus enum shows the production pattern: the transition matrix lives inside the type, one place, tested once, impossible to bypass.
Switching on Enums: Exhaustiveness for Free
The control flow article promised that switch expressions over enums enforce exhaustiveness, and enums are where that promise pays salaries. So, cover every constant and you may omit default. Add a REFUNDED constant next quarter, and every switch expression that misses it fails to compile, in every file, at once.
// WRONG: a statement switch silently does nothing for new constants
switch (status) {
case NEW: handleNew(); break;
case PAID: handlePaid(); break;
// add DELIVERED later and this switch ships a silent gap
}
// RIGHT: a switch expression fails the build until every constant is handled
String label = switch (status) {
case NEW -> "awaiting payment";
case PAID -> "preparing shipment";
case SHIPPED -> "in transit";
case DELIVERED -> "complete";
case CANCELLED -> "closed";
};
The delta: the statement form trusts you to remember; the expression form makes the compiler remember. In short, for state machines, that difference is the whole reliability model.
EnumSet and EnumMap: Bit-Level Speed
When the data is enum-typed, the JDK offers two purpose-built collections, documented in the EnumSet and EnumMap specs. EnumSet stores a set of constants in a bit vector: for enums with 64 or fewer constants, that is a single long. EnumMap stores values in an array indexed by ordinal, so a lookup is an array access, not a hash computation.
import java.time.DayOfWeek;
import java.util.EnumSet;
import java.util.EnumMap;
EnumSet<DayOfWeek> weekend = EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY);
EnumSet<DayOfWeek> workweek = EnumSet.range(DayOfWeek.MONDAY, DayOfWeek.FRIDAY);
var hours = new EnumMap<DayOfWeek, String>(DayOfWeek.class);
hours.put(DayOfWeek.MONDAY, "09:00-18:00");
hours.put(DayOfWeek.SATURDAY, "10:00-14:00");
System.out.println(weekend.contains(DayOfWeek.SUNDAY)); // true
System.out.println(hours.get(DayOfWeek.MONDAY)); // 09:00-18:00
The DayOfWeek constants come from java.time, which gets its full article in Part 3. Here they are just good enum citizens. The performance and behavior differences versus the general-purpose versions:
| Feature | HashSet / HashMap | EnumSet / EnumMap |
|---|---|---|
| Backing store | Hash table with per-entry objects | Bit vector / array indexed by ordinal |
| Lookup cost | Hash computation, then bucket walk | Bit mask test or array access |
| Memory for small sets | Entry objects plus table | A single long for up to 64 constants |
| Iteration order | Unspecified | Declaration order, always |
| Null keys | HashMap allows one | EnumMap rejects them |
| Use when | Keys or elements are general objects | The type is an enum |
The rule is simple: when a set or map is keyed by an enum type, choose the enum version. Overall, it is faster, smaller, iterates in declaration order, and communicates intent to every reader. The choosing the right collection article places these two in the wider decision tree in Part 3.
How Real Systems Do This
In fact, the JDK itself leans on enums for its own fixed sets, and you have used several already. java.time’s DayOfWeek and Month are enums, which is why EnumMap works so well with them. TimeUnit in java.util.concurrent is an enum that Part 5 will use constantly: pool.await(500, TimeUnit.MILLISECONDS). StandardCharsets, Thread.State, and the file-option flags in NIO (article 33) are enums too. In fact, that NIO API literally takes EnumSet.of(StandardOpenOption…) for its flags.
Meanwhile, production systems use enums mostly for state machines, exactly like OrderStatus above, and the payoff is compile-time closure. In my experience, the highest-value refactor I have run was replacing 27 string literals for a shipment status with one enum: the compiler immediately flagged four typos and two impossible transitions in our own code the same day, bugs that had shipped quietly under the string regime.
One design note for the boundary cases: enum singletons are also the safest singleton pattern in Java, because serialization and reflection cannot create a second instance. When you need one of something, a one-constant enum is an honest answer.
Decision Framework
- Is the set of variants fixed at compile time? Use an enum. If it grows at runtime, the sealed classes article offers the middle path.
- Do the variants carry data? Give them fields via the constructor. Behavior? Abstract methods with per-constant bodies, or a switch expression nearby.
- Is it a set of enum flags? EnumSet. A map keyed by enum constants? EnumMap.
- Will the value be stored or sent over the wire? Persist name(), parse with valueOf(), and never store ordinal().
- Is the “enum” actually a bag of unrelated constants? Instead, split it into the concepts it is mashing together.
When NOT to Use This
- Do not model open variant sets with enums. Plugin types, user-defined categories, and rows in a database table grow at runtime. However, an enum cannot, so use classes or sealed hierarchies over entities.
- Do not persist ordinal(). Reordering the constants, inserting one in the middle, or trimming another silently corrupts every stored value; store the name instead.
- Do not hide services inside enums. Validation and transitions belong in the enum. Orchestration, I/O, and dependency wiring do not, because they make the constants untestable and the service invisible.
Common Mistakes
- Persisting ordinal() and reordering constants later: every stored number now names a different status, silently.
- Using the colon-form switch over enums. Adding a constant ships a silent gap; the arrow form makes the build fail instead.
- Calling values() in a hot loop: it allocates a fresh array every call. So, capture it once if you iterate constantly.
- Treating enums as int constants with no fields or methods: half the type’s value is unused. As a result, switch logic drifts outside the enum.
- Using HashSet or HashMap for enum-typed keys and paying hashing costs, plus unordered iteration, for data the enum version handles better.
- Passing null as an EnumMap key and being surprised by NullPointerException: the enum collections document that null keys are rejected.
Key Takeaways
- An enum is a fixed set of named singletons; the compiler holds the set closed, and == is safe for comparisons.
- Constants can carry fields, constructors, and per-constant method bodies, making enums small behavior carriers.
- Switch expressions over enums enforce exhaustiveness; a new constant breaks every switch that missed it at build time.
- EnumSet stores up to 64 constants in one long. Similarly, EnumMap indexes an array by ordinal: both beat the hash versions for enum-typed data.
- Persist enum values by name(), never ordinal(), because declaration order is an implementation detail.
- Open variant sets that grow at runtime want sealed interfaces or entities, not enums.
- The JDK’s own enums, DayOfWeek, Month, TimeUnit, StandardCharsets, prove the type’s production range.
FAQ
What is an enum in Java?
A type with a fixed set of named instances, one object per constant, created once by the JVM. It replaces int and string constants with compile-time-checked names that carry behavior.
Can a Java enum have constructors and methods?
Yes. Constructors are implicitly private and run once per constant, fields carry per-constant data, and methods (including abstract ones with a body per constant) give each variant its own behavior.
What is the difference between EnumSet and HashSet?
EnumSet stores constants in a bit vector, so for enums of 64 or fewer constants it is a single long: contains and add are bit operations with no hashing, and iteration follows declaration order. HashSet is general but pays hash costs for the same data.
Can I use == to compare enums in Java?
Yes, and it is the preferred comparison: each constant is a singleton, so reference equality is content equality. The comparison is also null-safe in the usual direction (constant == null is false).
What is ordinal() in a Java enum?
The constant’s declaration position, starting at 0. It is useful for internal tricks like EnumMap’s indexing, but it must never be persisted or sent over the wire, because reordering the declaration silently changes its meaning.
Conclusion
Enums give Java its safest modeling tool: closed variant sets with data, behavior, and compiler-enforced exhaustiveness, plus two purpose-built collections that store them at bit-level speed. For state machines and fixed vocabularies, they eliminate the typo bugs and missing-case bugs that string and int constants guarantee.
The next article covers records, the modern counterpart for pure data: where enums fix the set of values, records fix the shape of values, and together they replace most of the ceremony classes you used to write.
When the set is closed, let the compiler hold it closed. Every bug it blocks is a page you never read at 3 a.m.
Last updated on 6 September 2026.
