Most Java code uses enum as a typed list of names: enum Day { MON, TUE }, a switch over it, and nothing else. That is a correct use, and it is a small corner of what the construct offers.
An enum declaration is a class declaration. It compiles to a class extending java.lang.Enum, each constant is one instance of that class, and almost everything a class can carry — fields, a constructor, methods, an implemented interface, even a separate method body per constant — an enum can carry too. This article works through all of it and checks each claim against the compiler.
![]()
Every listing below was compiled and run on OpenJDK 21.0.6 (arm64). Every javap dump, class-file listing, compiler error and exception message is copied from that run rather than written from memory.
What an enum really is
Start with the declaration everyone writes.
enum Day { MON, TUE, WED }
javac Day.java produces Day.class, and javap Day.class prints the public shape of it:
Compiled from "Day.java"
final class Day extends java.lang.Enum<Day> {
public static final Day MON;
public static final Day TUE;
public static final Day WED;
public static Day[] values();
public static Day valueOf(java.lang.String);
static {};
}
Three lines of source became a final class with three public static final fields of its own type. Add -p and the private members appear as well:
Compiled from "Day.java"
final class Day extends java.lang.Enum<Day> {
public static final Day MON;
public static final Day TUE;
public static final Day WED;
private static final Day[] $VALUES;
public static Day[] values();
public static Day valueOf(java.lang.String);
private Day();
private static Day[] $values();
static {};
}

Everything below the three constants was written by the compiler. $VALUES is the array that actually holds them, $values() is the helper that builds it, values() and valueOf(String) are the public accessors, private Day() is the constructor you did not declare, and static {} is the class initialiser that constructs each constant in declaration order.
javap hides two parameters on the constructor. Reflection does not:
for (Constructor<?> c : Day.class.getDeclaredConstructors()) {
System.out.println(c);
}
private Day(java.lang.String,int)
Every enum constructor really takes a name and an ordinal in front of your own parameters, and passes them straight up to java.lang.Enum. That is where name() and ordinal() get their values.
The generated values() is thinner than it looks. javap -c -p Day.class shows its body:
public static Day[] values();
Code:
0: getstatic #13 // Field $VALUES:[LDay;
3: invokevirtual #17 // Method "[LDay;".clone:()Ljava/lang/Object;
6: checkcast #18 // class "[LDay;"
9: areturn
It returns $VALUES.clone() — a fresh copy on every call. Remember that; it comes back as a trap at the end.
Three consequences follow directly from the compiled shape:
- The class is
final, so nothing may subclass it.class E2 extends Day { }fails twice:cannot inherit from final Dayandenum classes are not extensible. - The constant set is fixed at compile time.
new Colour()is rejected withenum classes may not be instantiated. - An enum already extends
java.lang.Enum, so it cannot extend anything else.enum E1 extends Base { A }does not even parse;javacreports'{' expectedat theextends.
Fields, constructors and methods on an enum
Because a constant is an instance, it can carry state, and the values are supplied in the constant list.
enum Status {
OK(200, "OK"),
MOVED(301, "Moved Permanently"),
NOT_FOUND(404, "Not Found"),
SERVER_ERROR(500, "Internal Server Error");
private final int code;
private final String reason;
Status(int code, String reason) {
this.code = code;
this.reason = reason;
}
public int code() { return code; }
public boolean isError() { return code >= 400; }
@Override
public String toString() { return code + " " + reason; }
}
OK -> 200 OK, error=false
MOVED -> 301 Moved Permanently, error=false
NOT_FOUND -> 404 Not Found, error=true
SERVER_ERROR -> 500 Internal Server Error, error=true
The semicolon after the last constant is what separates the constant list from the class body, and it is required as soon as anything follows the constants. javap -p Status.class confirms the fields and methods are exactly where you put them:
final class Status extends java.lang.Enum<Status> {
public static final Status OK;
public static final Status MOVED;
public static final Status NOT_FOUND;
public static final Status SERVER_ERROR;
private final int code;
private final java.lang.String reason;
private static final Status[] $VALUES;
public static Status[] values();
public static Status valueOf(java.lang.String);
private Status(int, java.lang.String);
public int code();
public boolean isError();
public java.lang.String toString();
private static Status[] $values();
static {};
}
Note that the constructor is private even though the source declares no modifier at all. It is implicitly private, and the compiler refuses any attempt to widen it:
PubCtor.java:6: error: modifier public not allowed here
public PubCtor(int n) { this.n = n; }
^
1 error
protected fails the same way with modifier protected not allowed here. Writing private explicitly is legal but redundant. The reason is the whole point of the construct: if a caller could invoke the constructor, the constant set would no longer be the complete list of instances, and every guarantee that follows from that — == comparison, EnumSet, exhaustive switch, the singleton — would collapse.
The constructor runs before any static field of the enum is initialised, because the constants themselves are the first thing the class initialiser builds. So this does not compile:
enum Code {
A(1), B(2);
private static final Map<Integer, Code> BY_NUM = new HashMap<>();
private final int num;
Code(int num) {
this.num = num;
BY_NUM.put(num, this); // BY_NUM does not exist yet
}
}
StaticInit.java:8: error: illegal reference to static field from initializer
BY_NUM.put(num, this);
^
1 error
Build the lookup in a static block instead, which runs after every constant exists:
enum Code {
A(1), B(2), C(3);
private static final Map<Integer, Code> BY_NUM = new HashMap<>();
static {
for (Code c : values()) BY_NUM.put(c.num, c);
}
private final int num;
Code(int num) { this.num = num; }
public static Optional<Code> fromNum(int n) { return Optional.ofNullable(BY_NUM.get(n)); }
}
Optional[B]
Optional.empty
Constant-specific class bodies
This is the feature most tutorials never reach. A constant may be followed by a brace block, and inside that block it can override a method of the enum for itself alone.
enum Op {
PLUS("+") {
@Override public double apply(double a, double b) { return a + b; }
},
MINUS("-") {
@Override public double apply(double a, double b) { return a - b; }
},
TIMES("*") {
@Override public double apply(double a, double b) { return a * b; }
},
DIVIDE("/") {
@Override public double apply(double a, double b) {
if (b == 0) throw new ArithmeticException("divide by zero");
return a / b;
}
};
private final String symbol;
Op(String symbol) { this.symbol = symbol; }
public abstract double apply(double a, double b);
@Override public String toString() { return symbol; }
}
12.0 + 4.0 = 16.0
12.0 - 4.0 = 8.0
12.0 * 4.0 = 48.0
12.0 / 4.0 = 3.0
Each of those bodies is compiled as an anonymous subclass of the enum, so javac Op.java writes six class files, not two:
Op$1.class
Op$2.class
Op$3.class
Op$4.class
Op.class
OpMain.class

javap -p on the enum and on the first of those files shows what happened:
abstract class Op extends java.lang.Enum<Op> {
public static final Op PLUS;
...
private Op(java.lang.String);
public abstract double apply(double, double);
}
final class Op$1 extends Op {
private Op$1(java.lang.String, int, java.lang.String);
public double apply(double, double);
}
Two things changed. Op is now abstract rather than final, because two constants run different code and the enum type itself has no single body to offer. And each constant body is a final subclass of Op. Note that javap prints the subclass constructor in full — (String, int, String) — because it only elides the synthetic name and ordinal on the enum class itself. The practical result is that the constants no longer share one runtime class:
Op.PLUS.getClass() = class Op$1
Op.PLUS.getDeclaringClass = class Op
superclass of PLUS body = class Op
getClass() gives the generated subclass; getDeclaringClass() gives the enum type, which is what you want in almost every case where the distinction matters.
One limit is worth knowing before you reach for it. A constant body can override, but it cannot add to the public surface of the enum. Declaring a brand new method inside a body — A { public void extra() { ... } } — makes it unreachable through the constant:
E5.java:6: error: cannot find symbol
public static void main(String[] x) { E5.A.extra(); }
^
symbol: method extra()
location: variable A of type E5
1 error
The static type of E5.A is E5, and E5 has no extra(). Whatever a body implements must be declared on the enum, or inherited from an interface it implements.
Abstract methods force every constant to answer
Declaring the method abstract on the enum, as Op does, turns the compiler into the enforcer: every constant must supply a body, and adding a constant without one breaks the build immediately.
enum E3 {
A { @Override public int v() { return 1; } },
B;
public abstract int v();
}
E3.java:3: error: E3 is abstract; cannot be instantiated
B;
^
1 error
The error points at B, the constant that failed to supply an implementation. If no constant has a body at all, the message is the one any class gets for an unimplemented abstract method:
E4.java:1: error: E4 is not abstract and does not override abstract method v() in E4
enum E4 {
^
1 error
That is the practical argument for an abstract method over a switch inside a concrete method. A switch that forgets a new constant may only fail at runtime; an abstract method cannot be forgotten, because the file will not compile.
An enum can implement an interface
An enum cannot extend a class, but it may implement as many interfaces as it wants — including with default methods, which every constant inherits.
interface Priced {
int cents();
default String display() { return String.format("%d.%02d", cents() / 100, cents() % 100); }
}
enum Coin implements Priced {
PENNY(1), NICKEL(5), DIME(10), QUARTER(25);
private final int cents;
Coin(int cents) { this.cents = cents; }
@Override public int cents() { return cents; }
}
QUARTER = 0.25
DIME = 0.10
total cents = 35
Coin.DIME instanceof Priced -> true
javap Coin.class shows the interface on the class declaration next to the implicit superclass:
final class Coin extends java.lang.Enum<Coin> implements Priced {
public static final Coin PENNY;
...
public int cents();
}
That combination is what makes an enum a usable member of an ordinary design: a fixed set of instances that still satisfies an interface the rest of the code depends on. A constant body may also implement the interface method per constant, which combines this section with the previous one.
EnumMap and EnumSet, and why they are not HashMap and HashSet
java.util ships two collections that exist only for enum keys. They are not a micro-optimisation of HashMap and HashSet; they have a different representation and a stronger contract.
An EnumMap is one array with a slot for every constant of the enum, indexed by ordinal(). Reflection into the private vals field shows exactly that — a map holding two entries over a seven-constant enum still owns seven slots:
map.size() = 2
vals.length = 7 (Day.values().length = 7)
vals[0] (MON) = null
vals[1] (TUE) = null
vals[2] (WED) = gym
vals[3] (THU) = null
vals[4] (FRI) = null
vals[5] (SAT) = rest
vals[6] (SUN) = null
There is no hashing, no bucket array, no collision handling and no stored Map.Entry node per key: a lookup is an array index. The visible consequence is iteration order. Put the same four keys into both maps in the same deliberately scrambled order and read them back:
Map<Day, Integer> em = new EnumMap<>(Day.class);
Map<Day, Integer> hm = new HashMap<>();
for (Day d : new Day[]{ Day.SUN, Day.WED, Day.MON, Day.FRI }) {
em.put(d, d.ordinal());
hm.put(d, d.ordinal());
}
EnumMap : {MON=0, WED=2, FRI=4, SUN=6}
HashMap : {WED=2, FRI=4, SUN=6, MON=0}
The EnumMap iterates in declaration order and the specification guarantees it will. The HashMap order is whatever the hash codes produced on this run; it is unspecified, so it is not something to rely on even when it looks stable.
EnumSet is the same idea with a bit vector instead of an array. Which implementation you get depends on the size of the enum, and the class name says so:
64 constants -> java.util.RegularEnumSet
65 constants -> java.util.JumboEnumSet
RegularEnumSet keeps the whole set in a single long, one bit per constant, which is why the boundary is exactly 64. JumboEnumSet uses an array of long. Neither has a public constructor; you build them through factories, and they too iterate in declaration order regardless of insertion order:
EnumSet<Day> s = EnumSet.noneOf(Day.class);
s.add(Day.SUN); s.add(Day.TUE); s.add(Day.FRI); s.add(Day.MON);
EnumSet : [MON, TUE, FRI, SUN]
LinkedHashSet : [SUN, TUE, FRI, MON]
HashSet : [MON, TUE, SUN, FRI]
The factories cover the useful shapes directly:
weekend = [SAT, SUN]
week = [MON, TUE, WED, THU, FRI]
range = [TUE, WED, THU]
built by EnumSet.of(Day.SAT, Day.SUN), EnumSet.complementOf(weekend) and EnumSet.range(Day.TUE, Day.THU). EnumSet.allOf and EnumSet.noneOf complete the set.
EnumMap | HashMap | |
|---|---|---|
| Representation | one array indexed by ordinal() | bucket array plus a node per entry |
| Lookup | array index | hash, bucket, then equals |
| Iteration order | declaration order, guaranteed | unspecified |
null key | NullPointerException | permitted |
| Key type | fixed at construction, from Day.class | any object |
The null rejection is real and immediate: em.put(null, 0) throws NullPointerException because there is no array index for a key that has no ordinal, while hm.put(null, 0) is accepted. Choose EnumMap and EnumSet for the contract — deterministic order, a key type fixed at construction, no reliance on hashCode — and treat the compact representation as the reason those guarantees are cheap to provide.
switch over an enum, and exhaustiveness
Inside a case label the constant is written unqualified, because the type is already known from the selector:
static String action(Signal s) {
return switch (s) {
case RED -> "stop";
case AMBER -> "prepare";
case GREEN -> "go";
};
}
Java 21 also accepts the qualified form, case Signal.RED ->, which compiles and behaves identically. The arrow form takes a single expression or a braced block, and never falls through, so no break is needed:
static int seconds(Signal s) {
switch (s) {
case RED -> { return 30; }
case AMBER -> { return 4; }
case GREEN -> { return 25; }
}
return -1;
}
RED: stop for 30s
AMBER: prepare for 4s
GREEN: go for 25s
The important difference between the two forms above is not the syntax, it is that one of them is a switch expression. A switch expression over an enum must be exhaustive, and the compiler checks it:
Sw2.java:5: error: the switch expression does not cover all possible input values
return switch (s) {
^
1 error
A switch statement has no such requirement. Leave a constant out and it compiles silently, with no diagnostic even under -Xlint:all on OpenJDK 21.0.6, and simply does nothing at runtime for the missing case.
That asymmetry decides how to write the code. Prefer the expression form, and do not add a default branch when you have covered every constant: the default would satisfy the exhaustiveness check forever, and the day someone adds a constant the build stays green while the new constant quietly takes the default path. Without default, adding a constant breaks compilation at every switch that has to be updated, which is exactly the reminder you want.
The enum singleton
A single-constant enum gives you an instance the language guarantees is unique, and the guarantee holds against the two mechanisms that break a hand-written singleton.
enum Registry {
INSTANCE;
private int hits;
public int hit() { return ++hits; }
public int hits() { return hits; }
}
Serialization first. Write Registry.INSTANCE to an ObjectOutputStream, read it back, and compare identity:
hits = 2
deserialized == INSTANCE ? true
hits after round trip = 2
The stream carries only the constant name, and deserialization resolves it back through valueOf on the enum type, so no second object is ever constructed. A plain Serializable singleton has to write a readResolve method to get the same result, and forgetting it is a common way to end up with two instances.
Reflection is the more interesting case. Enum constructors are private, but setAccessible(true) normally defeats that:
Constructor<?> c = Registry.class.getDeclaredConstructors()[0];
c.setAccessible(true);
Object rogue = c.newInstance("ROGUE", 1);
declared constructors: [private Registry(java.lang.String,int)]
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
Constructor.newInstance checks the ENUM modifier on the declaring class and refuses before it allocates anything. The check is in the JDK, not in your code, so there is nothing to forget. Contrast that with a class-based singleton, where exactly the same three lines succeed:
final class Plain {
static final Plain INSTANCE = new Plain();
private Plain() {}
}
rogue == Plain.INSTANCE ? false
Two objects, and Plain.INSTANCE is no longer the only one. Only the enum form is closed against this by the language itself.
Two honest limits. The uniqueness is about identity, not about concurrency — a mutable field on the constant needs the same synchronisation any shared object needs. And a single-constant enum cannot extend a class, so if the type has to sit under an existing base class this is not available; an interface it can implement freely.
A state machine built from an enum
Everything above combines into one of the most useful things an enum can be: a set of states where each constant knows which transitions are legal.
enum OrderState {
NEW {
@Override public Set<OrderState> allowed() { return EnumSet.of(PAID, CANCELLED); }
},
PAID {
@Override public Set<OrderState> allowed() { return EnumSet.of(SHIPPED, CANCELLED); }
},
SHIPPED {
@Override public Set<OrderState> allowed() { return EnumSet.of(DELIVERED); }
},
DELIVERED {
@Override public Set<OrderState> allowed() { return EnumSet.noneOf(OrderState.class); }
},
CANCELLED {
@Override public Set<OrderState> allowed() { return EnumSet.noneOf(OrderState.class); }
};
public abstract Set<OrderState> allowed();
public boolean isTerminal() { return allowed().isEmpty(); }
public OrderState to(OrderState target) {
if (!allowed().contains(target)) {
throw new IllegalStateException(
"illegal transition " + this + " -> " + target + "; allowed from " + this + ": " + allowed());
}
return target;
}
}

The abstract allowed() is what makes this safe: add a sixth state and the file stops compiling until that state declares where it may go. Running a legal path and then an illegal move gives:
start: NEW
-> PAID
-> SHIPPED
-> DELIVERED (terminal)
rejected: illegal transition DELIVERED -> CANCELLED; allowed from DELIVERED: []
transition table:
NEW -> [PAID, CANCELLED]
PAID -> [SHIPPED, CANCELLED]
SHIPPED -> [DELIVERED]
DELIVERED -> []
CANCELLED -> []
| State | allowed() | Terminal |
|---|---|---|
NEW | [PAID, CANCELLED] | no |
PAID | [SHIPPED, CANCELLED] | no |
SHIPPED | [DELIVERED] | no |
DELIVERED | [] | yes |
CANCELLED | [] | yes |
The obvious alternative — pass the target set into the constructor — does not compile, and the reason is the construction order from the second section:
enum Fwd {
A(EnumSet.of(Fwd.B)),
B(EnumSet.noneOf(Fwd.class));
...
}
Fwd.java:4: error: illegal forward reference
A(EnumSet.of(Fwd.B)),
^
1 error
B does not exist while A is being constructed. Either return the set from an overridden method as above, which is evaluated lazily on call, or build a static EnumMap of transitions in a static block after all the constants exist. The method form keeps each state's rules next to the state, which is the point of doing this with an enum at all.
Traps
ordinal() used as a stored value. The ordinal is a position in the source file, not an identity. Persist it and the meaning of your data changes the day someone inserts a constant. With enum Level { LOW, HIGH } a stored 1 reads back as HIGH:
0 = LOW
1 = HIGH
row stored as 1 reads back as: HIGH
Insert MEDIUM in the middle and every stored 1 silently becomes something else:
0 = LOW
1 = MEDIUM
2 = HIGH
row stored as 1 reads back as: MEDIUM
Nothing failed, no exception was thrown, and the data is now wrong. Persist name() and read it back with valueOf, or give the enum an explicit code field you control. The Javadoc for ordinal() says outright that it is designed for EnumSet and EnumMap and that most programmers will have no use for it.
values() hands you a fresh array every call. Because the generated method returns $VALUES.clone(), two calls are different objects and writing into the result is discarded:
first == second ? false
Arrays.equals(f, s) ? true
after first[0] = WED : [WED, TUE, WED]
next Day.values() call : [MON, TUE, WED]
That is what protects the constant array from being corrupted by a caller, and it also means values() allocates. Calling it inside a loop copies the array on every iteration; hoist it into a private static final array, or iterate EnumSet.allOf instead, when the call is on a hot path.
equals versus ==. Both work, and == is better for three separate reasons. It is null-safe, where equals on a null reference throws:
d == Day.TUE ? true
d.equals(Day.TUE) ? true
nil == Day.TUE ? false
nil.equals(Day.TUE) -> NullPointerException
It is also type-checked at compile time. Comparing constants of two different enum types with == fails the build:
Cross.java:5: error: incomparable types: A1 and B1
System.out.println(A1.X == B1.Y);
^
1 error
whereas Day.MON.equals(Month.JAN) compiles happily and quietly returns false — a comparison that is always wrong and never complains. Enum.equals is final and implemented as this == other, so there is no behaviour to gain by calling it.
valueOf throws, and it is case-sensitive. It is not a parser and it does not return null:
java.lang.IllegalArgumentException: No enum constant Day.FUNDAY
No enum constant Day.mon
For input you do not control, wrap it in try/catch or look the value up in a static Map built in a static block, as Code.fromNum does above.
FAQ
What does a Java enum actually compile to?
A class. enum Day { MON, TUE, WED } becomes final class Day extends java.lang.Enum<Day> with one public static final Day field per constant, a private constructor whose real signature is (String name, int ordinal) plus your own parameters, a private $VALUES array, the generated values() and valueOf(String) methods, and a class initialiser that constructs every constant in declaration order. javap -p on the class file shows all of it.
Can a Java enum have a constructor and fields?
Yes. Put the arguments in parentheses after each constant, end the constant list with a semicolon, and declare the fields and constructor below it. The constructor is implicitly private and cannot be made public or protected — javac answers modifier public not allowed here. It also runs before any static field of the enum is initialised, so referring to one from the constructor fails with illegal reference to static field from initializer; build such state in a static block instead.
What is a constant-specific class body?
A brace block written directly after a constant, containing method overrides that apply to that constant only. javac compiles each one as an anonymous subclass, so an enum with four constant bodies emits Op.class, Op$1.class through Op$4.class, the enum class becomes abstract rather than final, and Op.PLUS.getClass() reports class Op$1 while getDeclaringClass() still reports class Op. A body may override methods declared on the enum or inherited from its interfaces; a brand new method declared inside a body is not reachable through the constant.
Why use EnumMap instead of HashMap?
Because of what it guarantees, not because of a benchmark. An EnumMap is a single array indexed by ordinal(), so it iterates in declaration order and the specification guarantees that order, it needs no hashCode or equals on the key, it stores no per-entry node, and it rejects a null key with NullPointerException. A HashMap guarantees no iteration order at all. When the key type is an enum, the EnumMap is both the stronger contract and the smaller structure.
Is an enum the best way to write a singleton in Java?
It is the only form the language itself closes. A single-constant enum survives a serialization round trip as the same object with no readResolve to remember, and Constructor.newInstance on it fails with java.lang.IllegalArgumentException: Cannot reflectively create enum objects where the identical reflection call on a class with a private constructor succeeds and produces a second instance. The limits are that it cannot extend a class and that uniqueness says nothing about thread safety for mutable state on the constant.
Should I store ordinal() in a database?
No. ordinal() is the position of the constant in the source file, so inserting or reordering a constant reassigns the meaning of every value already stored, silently and without any error. Store name() and read it back with valueOf, or declare an explicit code field on the enum and persist that. Keep ordinal() for what it is for, which is indexing EnumMap and EnumSet internally.
Do I need a default branch in a switch over an enum?
Not when you cover every constant, and leaving it out is better. A switch expression over an enum must be exhaustive, so if a constant is missing the build fails with the switch expression does not cover all possible input values. Adding default satisfies that check permanently, so a constant added later slips into the default branch with no warning. A switch statement is not checked at all and stays silent even under -Xlint:all, which is a good reason to prefer the expression form.
Conclusion
An enum is a class whose instances are fixed at compile time, and every advanced feature is a consequence of that one fact. Because the constants are instances, they can hold fields and be built by a constructor. Because the constructor is private and the class initialiser is the only caller, the constant list really is the complete set of instances — which is what lets == replace equals, EnumSet fit in a long, EnumMap be an array, a switch expression be checked for exhaustiveness, and a one-constant enum be a singleton that reflection cannot forge. Because a constant is an object, it can carry its own method body, and an abstract method turns "every constant must decide this" into a compile error rather than a code review comment.
That is also what makes the enum state machine worth writing: the transitions live on the states, the compiler refuses to let a new state skip them, and an illegal move fails loudly instead of corrupting a record. The costs are small and worth knowing — an extra class file per constant body, a fresh array from every values() call, and an ordinal() that must never leave the process.
Next in this series: generics — type parameters, bounded types, wildcards, and what type erasure really removes from your compiled code.