enums
type-safe constants that are secretly full classes.
- chapter
- chapter 03.10
- reading time
- 7 min read
an enum is a class with a fixed list of instances, created once when the class is initialized. each constant is a public static final field pointing at a singleton object. that's why == works on enums, why they can have fields, constructors and methods, and why each constant can even override a method with its own body.
the picture
you know the old way: static final int RED = 0, GREEN = 1;. nothing stops someone passing 42, printing gives you a number, and RED + GREEN compiles. kinda cursed.
an enum is a guest list for a party where the door is locked once everyone's in. the class decides exactly who exists (MERCURY, VENUS, EARTH), makes each of them once, and never lets anyone else in. there's no public constructor, no new Planet(), no way to sneak a fourth planet in at runtime (not even with reflection or deserialization).
$VALUES, that backs values()). a variable like home is just another reference to one of them. (each also has a name field pointing at its String, left out to keep it readable.)what javac actually writes
enum Planet {
MERCURY(3.303e23, 2.4397e6),
VENUS(4.869e24, 6.0518e6),
EARTH(5.976e24, 6.37814e6);
private final double mass; // kg
private final double radius; // m
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
}
double surfaceGravity() {
return 6.67300E-11 * mass / (radius * radius);
}
}javap -p Planet shows what that turned into:
final class Planet extends java.lang.Enum<Planet> {
public static final Planet MERCURY;
public static final Planet VENUS;
public static final Planet EARTH;
private final double mass;
private final double radius;
private static final Planet[] $VALUES;
public static Planet[] values();
public static Planet valueOf(java.lang.String);
private Planet(double, double);
double surfaceGravity();
private static Planet[] $values();
static {};
}so an enum is:
- a final class extending
java.lang.Enum(which is why an enum can't extend anything else, but can implement interfaces). - one
public static finalfield per constant, filled in bystatic {}, which runsnew Planet("MERCURY", 0, 3.303e23, 2.4397e6)and so on. the constructor secretly takes the name and ordinal first: its real descriptor is(String, int, double, double). - a private constructor. you couldn't call it if you tried.
values()andvalueOf(String), generated for you.
all the static init rules apply: the constants are created once, the first time the class is used, and everyone after that shares them.
the built-in methods
enum Size { SMALL, MEDIUM, LARGE }
void main() {
IO.println(Size.valueOf("MEDIUM").ordinal() + " " + (Size.LARGE.compareTo(Size.SMALL) > 0) + " " + (Size.valueOf("SMALL") == Size.SMALL));
IO.println(Size.values() == Size.values());
IO.println(java.util.Arrays.toString(Size.values()) + " " + Size.MEDIUM.name());
}1 true true false [SMALL, MEDIUM, LARGE] MEDIUM
| method | what it does |
|---|---|
name() | the constant's name exactly as declared. toString() returns the same unless you override it |
ordinal() | its position, from 0. assigned by declaration order |
compareTo(o) | negative, zero or positive by declaration order. enums are Comparable that way |
values() | a new copy of the constants array, every call (see the false above) |
valueOf("X") | the constant named X, case-sensitive. unknown name: IllegalArgumentException: No enum constant Size.X |
== is the right way to compare enums. there's only ever one Size.SMALL, so identity and equality are the same thing, and == can't throw on null.
enum Size { SMALL, MEDIUM, LARGE }
IO.println(Size.valueOf("MEDIUM").ordinal() + " "
+ (Size.LARGE.compareTo(Size.SMALL) > 0) + " "
+ (Size.valueOf("SMALL") == Size.SMALL));constants with their own behaviour
a constant can have a body. each one becomes an anonymous subclass of the enum, so it can override methods, and the enum can declare abstract ones that every constant must implement:
enum Op {
PLUS("+") {
int apply(int a, int b) { return a + b; }
},
TIMES("*") {
int apply(int a, int b) { return a * b; }
};
final String symbol;
Op(String symbol) { this.symbol = symbol; }
abstract int apply(int a, int b);
}
void main() {
for (Op op : Op.values()) {
IO.println("2 " + op.symbol + " 3 = " + op.apply(2, 3));
}
}2 + 3 = 5 2 * 3 = 6
compile that and you get Op.class, Op$1.class and Op$2.class: one class per constant body. that's polymorphism with a closed, known set of subclasses.
switching on enums
the arrow form of switch checks that you covered every constant. no default needed, and adding a constant later breaks the build at every switch that forgot it, which is exactly what you want:
enum Light { RED, YELLOW, GREEN, BLINKING }
void main() {
Light l = Light.RED;
String action = switch (l) {
case RED -> "stop";
case YELLOW -> "slow down";
case GREEN -> "go";
};
}error: the switch expression does not cover all possible input values. more on this in switch and pattern matching.
EnumSet and EnumMap
because the constants are known and numbered, the JDK can store sets and maps of them in the cheapest possible way. an EnumSet is a bit vector: one long, bit i set when the constant with ordinal i is in the set.
long below, and contains is a single AND. amber is the bit you touched last. enums with more than 64 constants get a long[] instead.an EnumMap is the same idea for maps: a plain array of values, indexed by ordinal. put(WEDNESDAY, "swim") is vals[2] = "swim".
both are faster and smaller than HashSet / HashMap for enum keys, and iterate in declaration order. use them. more in sets.
- an enum is a final class with a fixed set of singleton instances.
- each constant is a
public static finalfield, created once in static init. - enums can have fields, constructors, methods, and per-constant bodies.
- compare with
==. switch on them, and let the compiler check you covered every case. - EnumSet is a bit vector, EnumMap is an array. never persist
ordinal().