Enums
Type-safe constants. Methods on enum values. The trick of using an enum as a singleton. Java's enum is more powerful than most people realise.
More than named constants
Most introductions to Java enums describe them as "a list of named values":
public enum Direction { NORTH, SOUTH, EAST, WEST }
Then move on. That's fair — most enums you write really are just lists of named values. But Java's enum is more sophisticated than C's, more useful than a list of final int constants, and capable of things you can't do in most other languages. Worth understanding properly.
This chapter covers the basics, then the features that make Java's enum genuinely powerful: methods, fields, abstract methods per value, singletons, and EnumMap/EnumSet.
Basic syntax
Declare an enum at the same level as a class:
public enum Direction {
NORTH, SOUTH, EAST, WEST
}
Use the values:
Direction d = Direction.NORTH;
switch (d) {
case NORTH -> moveUp();
case SOUTH -> moveDown();
case EAST -> moveRight();
case WEST -> moveLeft();
}
// Iterate all values
for (Direction dir : Direction.values()) {
System.out.println(dir);
}
// Parse from string
Direction parsed = Direction.valueOf("NORTH");
**Why this is better than static final int constants:**
The pre-1.5 pattern:
public class Direction {
public static final int NORTH = 0;
public static final int SOUTH = 1;
// ...
}
int d = Direction.NORTH;
moveInDirection(d);
Problems with this pattern:
- **Not type-safe.** int d could be any int, not just a Direction value.
- **No iteration.** Can't easily loop over all values.
- **Bad debugging.** Prints 0 instead of NORTH.
- **Brittle equality.** compass.equals(direction) doesn't work if they're both ints from different "enums".
Real Java enums fix all of this. Use enums for any closed set of named values.
Enums with fields and methods
An enum value isn't just a name — it's a fully-fledged object. You can give it fields, constructors, and methods.
public enum HttpStatus {
OK(200, "Success"),
NOT_FOUND(404, "Not Found"),
SERVER_ERROR(500, "Internal Server Error"),
BAD_REQUEST(400, "Bad Request");
private final int code;
private final String message;
HttpStatus(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() { return code; }
public String getMessage() { return message; }
public boolean isError() {
return code >= 400;
}
}
Usage:
HttpStatus s = HttpStatus.NOT_FOUND;
System.out.println(s.getCode()); // 404
System.out.println(s.getMessage()); // "Not Found"
System.out.println(s.isError()); // true
The constructor is called once per enum value at class-load time. Enum constructors are implicitly private — you can't new HttpStatus(...) from outside.
**Why this matters.** A Direction doesn't need fields, but HttpStatus, Currency, PaymentMethod, Role, OrderStatus — these all benefit from carrying associated data. Putting that data in the enum (instead of an external lookup table) keeps it cohesive.
**Common pattern:** reverse lookup from a field value.
public enum HttpStatus {
OK(200), NOT_FOUND(404), SERVER_ERROR(500);
private final int code;
private static final Map<Integer, HttpStatus> BY_CODE = new HashMap<>();
static {
for (HttpStatus s : values()) {
BY_CODE.put(s.code, s);
}
}
HttpStatus(int code) { this.code = code; }
public static HttpStatus fromCode(int code) {
HttpStatus s = BY_CODE.get(code);
if (s == null) throw new IllegalArgumentException("unknown code: " + code);
return s;
}
}
Per-value behaviour with abstract methods
Java enums can declare abstract methods that each value implements differently. This is genuinely unusual — most languages can't do this:
public enum Operator {
PLUS {
@Override public int apply(int a, int b) { return a + b; }
},
MINUS {
@Override public int apply(int a, int b) { return a - b; }
},
MULTIPLY {
@Override public int apply(int a, int b) { return a * b; }
},
DIVIDE {
@Override public int apply(int a, int b) {
if (b == 0) throw new ArithmeticException("divide by zero");
return a / b;
}
};
public abstract int apply(int a, int b);
}
Usage:
int result = Operator.PLUS.apply(3, 4); // 7
int result = Operator.DIVIDE.apply(10, 2); // 5
This pattern replaces a switch-statement-based approach:
// Instead of this anti-pattern:
public int evaluate(Operator op, int a, int b) {
switch (op) {
case PLUS: return a + b;
case MINUS: return a - b;
// ... etc — easy to forget a case, hard to extend
}
}
// Use this — exhaustive, type-safe, OO
public int evaluate(Operator op, int a, int b) {
return op.apply(a, b);
}
Adding a new operator means adding one enum constant with its implementation. No switch statement to update across the codebase.
EnumSet and EnumMap — the secret weapons
When your set or map uses enum keys, two specialised collections offer dramatic performance and memory wins.
**EnumSet<E>** — a Set of enum values, internally a single 64-bit long (for enums with ≤64 values) or a long[].
EnumSet<Day> weekdays = EnumSet.of(Day.MON, Day.TUE, Day.WED, Day.THU, Day.FRI);
EnumSet<Day> weekend = EnumSet.complementOf(weekdays);
weekdays.contains(Day.MON); // O(1), single bit test
weekdays.addAll(weekend); // O(1), single OR operation
A HashSet of Day values would use ~200 bytes per value. EnumSet uses 8 bytes total. Operations are bitwise — single CPU instructions.
**EnumMap<E, V>** — a Map with enum keys, backed by an array indexed by the enum's ordinal.
EnumMap<Day, String> schedule = new EnumMap<>(Day.class);
schedule.put(Day.MON, "Standup");
schedule.put(Day.WED, "Demo");
schedule.get(Day.MON); // O(1), direct array access
Compared to HashMap, EnumMap:
- Doesn't compute hashes.
- Doesn't deal with collisions.
- Iterates in enum declaration order (natural order).
- Uses far less memory.
**When to use them:** any time the key/element is an enum, prefer EnumSet/EnumMap over HashSet/HashMap. They're strictly better for that case.
Enums as singletons
A neat Java-specific pattern: use a single-value enum as a singleton.
public enum AppConfig {
INSTANCE;
private final Properties props = loadFromFile();
public String get(String key) {
return props.getProperty(key);
}
public void set(String key, String value) {
props.setProperty(key, value);
}
}
// Usage
String dbUrl = AppConfig.INSTANCE.get("db.url");
This is the most reliable way to write a singleton in Java. Reasons:
- **Thread-safe by default.** The JVM guarantees enum constants are initialised exactly once, atomically.
- **Serialization-safe.** Default serialization of enums preserves singleton identity. (Classic singleton patterns can be broken by serialization unless you implement
readResolve.) - **Reflection-safe.** You can't instantiate an enum via reflection — the JVM blocks it.
- **Concise.** One declaration, no boilerplate.
Joshua Bloch (author of "Effective Java" and the Collections framework) explicitly recommends this pattern over every other singleton approach.
Some developers find it weird. "Enums are for sets of values; using a one-value enum as a singleton is unusual." Fair point. But it works perfectly, and the trade-off is a tiny bit of cognitive friction for a lot of reliability. Worth knowing.
Comparing enums
Enum values are JVM-wide singletons — there's exactly one Day.MONDAY in memory. So == works for comparison:
if (day == Day.MONDAY) { ... } // safe — reference equality matches value equality
This is one of the only places where == on objects is the right choice. (The other being null checks.) .equals() works too but is unnecessary verbosity.
**Enums implement Comparable** — they compare by **ordinal** (declaration order):
Day.MON.compareTo(Day.WED); // negative (MON < WED in declaration order)
But beware: **don't rely on the ordinal in stored data.** If you reorder the enum values later, every saved ordinal becomes invalid. For persistence, use a stable string value or an explicit code field:
public enum OrderStatus {
PENDING("PND"),
PAID("PAID"),
SHIPPED("SHP"),
REFUNDED("RFN");
private final String code;
OrderStatus(String code) { this.code = code; }
public String getCode() { return code; }
}
Store getCode(), not ordinal(). Stable across enum changes.
Common pitfalls
**1. Using ordinal() in persistence.** Already covered. Use a stable string code.
**2. Putting heavyweight logic in enum constructors.** Enum constants are initialised at class-load time. If the constructor does I/O or anything that might fail, you get an obscure error at the moment any code first references the enum. Keep constructors trivial.
**3. Mutable state in enum values.**
public enum Counter {
INSTANCE;
private int count = 0; // shared mutable state — thread-unsafe by default
public void incr() { count++; }
}
Enum values are singletons, so any mutable state is shared. If multiple threads call incr(), you have a race. Either avoid mutable state in enums or synchronise explicitly.
**4. Comparing enum to string.**
if (day.equals("MONDAY")) { ... } // false — comparing Day to String
Use day == Day.MONDAY or day.name().equals("MONDAY") if you really need the string comparison.
**5. Forgetting to handle a new enum value in a switch.** Adding OrderStatus.RETURNED later breaks every switch that doesn't handle it. Modern switch expressions catch this at compile time when the switch is exhaustive (no default branch). Use exhaustive switches over enums to get compiler-checked completeness.
**6. Reflection.** It's possible to circumvent enum guarantees via deep reflection (Constructor.setAccessible(true), modifying private final fields). The JVM technically blocks Constructor.newInstance() on enum constructors, but the broader ecosystem assumes you're not malicious. For cryptographic or security-critical code, this matters; otherwise it doesn't.
⁂ Back to all modules