Enums

A fixed set of instances the compiler can check — with fields, methods and behaviour, plus EnumMap and EnumSet, which are faster than you expect.

10 min read Java Fundamentals

An enum is a class whose instances are fixed at compile time, and almost everything good about it follows from that one sentence. The compiler knows every value, so it can check a switch is exhaustive. The JVM creates each constant once, so == is correct. And because the constants are numbered, a set of them is a single long. This lesson is the type, the behaviour you can attach to it, and the two collections that exist only because enums are numbered.

A type with a known set of values

java
enum Status { PENDING, ACTIVE, SUSPENDED, CLOSED }

That is a class. Status.ACTIVE is a static final instance created once when the class loads, and there is no way to make another — the constructor is implicitly private and new Status() does not compile.

Two consequences arrive immediately, and both are things a String constant cannot give you:

  • A wrong value does not compile. Status s = "ACTIVE"; is a type error. With String constants it is a typo that reaches production.
  • == is correct here. One instance per constant means identity and equality coincide. This is the exception to the rule the equals lesson establishes, and the reason switch on an enum works at all.

Fields, constructors and methods

An enum is a class, so it can hold state and behaviour:

java
enum Plan {
    FREE(0, 3),
    PRO(1900, 50),
    ENTERPRISE(9900, Integer.MAX_VALUE);
 
    private final int paisePerMonth;
    private final int seatLimit;
 
    Plan(int paisePerMonth, int seatLimit) {
        this.paisePerMonth = paisePerMonth;
        this.seatLimit = seatLimit;
    }
 
    int monthlyPaise() { return paisePerMonth; }
    boolean allows(int seats) { return seats <= seatLimit; }
}

The constructor runs once per constant, at class initialisation, in declaration order. It is implicitly private; writing public there does not compile.

This is the shape that replaces a parallel Map<String, Integer> of prices, and it replaces it well: the data cannot drift out of sync with the constant, because it is written on the same line.

Constant-specific behaviour

When constants need different behaviour rather than different data, each can override:

java
enum Op {
    PLUS  { int apply(int a, int b) { return a + b; } },
    MINUS { int apply(int a, int b) { return a - b; } },
    TIMES { int apply(int a, int b) { return a * b; } };
 
    abstract int apply(int a, int b);
}

Each constant with a body is an anonymous subclass, which is why Op.PLUS.getClass() is Op$1 and not Op. That detail matters exactly twice: when something switches on getClass(), and when you notice values() returns objects whose classes differ.

The alternative is a switch inside one method. Prefer the override when the branches are genuinely different logic, and the switch when they are one expression with a few shapes — the override spreads the logic across the file, and that is a real cost.

switch, and the exhaustiveness nobody checks for you

An enum is the one type a switch was made for: the compiler knows every value, and the constants are the cases.

java
switch (status) {
    case PENDING:   return "Waiting";
    case ACTIVE:    return "Live";
    case SUSPENDED: return "On hold";
    case CLOSED:    return "Closed";
}
throw new IllegalStateException("unhandled status: " + status);

The throw at the end is not decoration. A switch statement over an enum is not checked for exhaustiveness. Add ARCHIVED to the enum later, forget this method, and the compiler says nothing — not an error, not a warning, not even with -Xlint:all. The method falls out of the bottom and returns whatever is written there.

That is the whole problem, and it is worth seeing rather than being told:

a constant added, the switch never updatedplaintext
$ javac -Xlint:all SwitchStmt.java
$ java SwitchStmt
ARCHIVED -> null

No warning at compile time, a null at run time, and a wrong label in production three weeks later. There is no flag that turns this on — javac -X lists every lint category it has, and exhaustiveness is not among them. (Eclipse's compiler does have one; if your team builds with ECJ, turn it on.)

So the discipline is the opposite of what it would be if the compiler helped:

The two ways to make the compiler help are both structural rather than syntactic. Put the behaviour on the enum — a constant-specific method, as in the section above — and adding a constant without implementing it is a compile error, because an abstract method must be implemented. Or keep an EnumMap from constant to handler and assert at startup that its keySet() equals EnumSet.allOf(...), which turns the gap into a failure at boot rather than at the unlucky request.

Later versions of Java do give the compiler the job: a switch expression over an enum must be exhaustive, so leaving default out makes adding a constant a build failure. The switch expression is covered in the control flow lesson; before Java 14, and in any switch statement, the throw is the tool you have.

values() hands you a copy

java
Status[] all = Status.values();    // a fresh array, every call
all[0] = null;                     // legal, and harmless

values() clones the backing array on every call, because arrays are mutable and the JVM cannot let you modify the canonical one. That makes it safe and makes it allocate — 48 bytes per call for a seven-constant enum, measured, which is a 16-byte array header plus seven references. Small, and it is per call: for (Status s : Status.values()) inside a request handler that runs a million times a day is 48 MB of garbage for a loop that could have read a static final array. Hoist it, or use EnumSet.allOf(Status.class).

valueOf("ACTIVE") returns the constant and throws IllegalArgumentException for anything else — including "active", because it is exact. For parsing external input, catch it or write a lookup that returns Optional.

Under the hood: ordinals, and the two collections built on them

Every constant carries an ordinal() — its zero-based position in the declaration. It is an implementation detail you should never persist (reordering the declaration silently changes it), and it is the reason EnumMap and EnumSet exist.

EnumMap is an array. Keyed by ordinal, no hashing, no Node objects, iteration in declaration order for free. It is what a HashMap<Status, X> wishes it were.

EnumSet is a long. For an enum with up to 64 constants the entire set is one 64-bit word: contains is a shift and an and, addAll is an or, complementOf is a not. Press Play and add seven days one at a time:

the long inside the EnumSet — bit n is ordinal n1MON01TUE11WED21THU31FRI41SAT51SUN6elements = 0x7Fcontains(c) is (elements >> c.ordinal()) & 1

7 of 7. Added THURSDAY — ordinal 3, so bit 3. elements = 0x7F. EnumSet 32 bytes, unchanged; a HashSet holding the same 7 would be 368.

7 / 7

They are added in no particular order, and the bits still light up in ordinal order — SATURDAY first turns on bit 5, not bit 0, and WEDNESDAY next fills a gap between two bits that are already set. The position is the constant's ordinal(), so an EnumSet has no order of its own to remember and iterating one comes out in declaration order for free.

The number that matters is the one that does not move: an EnumSet is 32 bytes whether it holds one constant or all of them, because the storage is the word, not the elements. A HashSet of the same seven is 368 bytes across ten objects.

Those 32 bytes are a 12-byte header, the long, and two references EnumSet keeps for the constant type and its full set of values — RegularEnumSet has three fields, not one, and counting only the ones a class declares is the commonest way to get an object's size wrong.

Walkthrough: the ordinal that shipped to the database

A service stored status.ordinal() in an INT column — compact, fast, and fine for two years.

Then somebody added ARCHIVED in alphabetical order, between ACTIVE and CLOSED. Every ordinal after it shifted by one. Ten million rows now meant something different from what they had meant the day before, silently, with no migration and no error: CLOSED accounts read back as PENDING.

Nothing in Java complained, because nothing was wrong in Java. The ordinal is a position, and the position changed.

The fix is to persist the name, or an explicit code the enum carries as a field:

java
enum Status {
    PENDING("P"), ACTIVE("A"), SUSPENDED("S"), CLOSED("C");
    private final String code;
    Status(String code) { this.code = code; }
    String code() { return code; }
}

JPA's @Enumerated(EnumType.ORDINAL) is the default, which is the same trap with a framework in front of it. @Enumerated(EnumType.STRING) costs a few bytes and cannot do this.

Try it yourself

What does it print?

java
enum Size { S, M, L }
Size a = Size.valueOf("M");
Size b = Size.M;
System.out.println((a == b) + " " + a.equals(b));
Answer

true true. valueOf returns the single existing instance rather than constructing anything, so identity and equality agree — the one case where == on objects is correct. Size.valueOf("m") would throw IllegalArgumentException: the lookup is exact.

Why will this not compile?

java
enum Status { PENDING, ACTIVE, CLOSED }
 
static int code(Status s) {
    switch (s) {
        case PENDING: return 1;
        case ACTIVE:  return 2;
    }
}
Answer

CLOSED is unhandled, so control can reach the end of the method without returning — and that is what the compiler refuses: missing return statement. It never mentions CLOSED.

The distinction matters. The compiler is not checking that you covered every constant; it is checking that every path returns. Change the method to void, or add a return 0; at the end, and it compiles silently with CLOSED falling through to whatever you wrote. The only error you can get here is an accident of the return type, which is why the throwing default is the tool rather than the compiler.

Which is smaller, and by how much?

EnumSet.of(MONDAY, WEDNESDAY, FRIDAY) against new HashSet<>(Arrays.asList(MONDAY, WEDNESDAY, FRIDAY)).

Answer

32 bytes against 240 — about seven and a half times. The EnumSet is a header, one long and two references; the HashSet is a wrapper around a HashMap with a 16-slot table and three 32-byte nodes. Add the other four days and the EnumSet does not grow at all while the HashSet reaches 368 bytes — the storage is the word, not the elements. That is the pair of numbers under the replay above: one of them never moves.

Misconceptions

  • "An enum is a list of constants." It is a class with a fixed set of instances; it can hold fields, implement interfaces and override methods per constant.
  • "ordinal() is a stable id." It is a declaration position. Reordering the constants changes it, and nothing warns you.
  • "values() is free." It clones the array on every call. In a loop that is an allocation per iteration.
  • "EnumSet is a Set implementation like any other." It is a bitmask, which is why it is both smaller and faster and why its iteration order is declaration order rather than insertion order.
  • "An enum cannot implement an interface." It can, and that is often the cleanest way to give a strategy a fixed, named set of implementations.

Going deeper

  • The Java Language Specification section 8.9, Enum Classes — the private constructor and implicit members are all written down there.
  • java.util.RegularEnumSet and JumboEnumSet in the JDK source: one long, and an array of long past 64 constants.
  • Effective Java, items 34 to 38 — the case for enums over int constants, and for EnumMap over ordinal indexing.
Progress is saved on this device and to your account when signed in.