skip to content

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).

stack
main
home
heap
class Planetstatic fields
MERCURY
VENUS
EARTH
$VALUES
Planet
ordinal0
mass3.303e23
Planet
ordinal1
mass4.869e24
Planet
ordinal2
mass5.976e24
Planet[]
0
1
2
three constants, three objects, created once by the class's static init and held by its static fields (plus a hidden array, $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

Planet.java
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:

text
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 final field per constant, filled in by static {}, which runs new 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() and valueOf(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

Main.java
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());
}
output
1 true true
false
[SMALL, MEDIUM, LARGE] MEDIUM
methodwhat 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.

predict the output
java
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:

Main.java
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));
    }
}
output
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:

Main.java
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.

EnumSet.of(MONDAY, WEDNESDAY, FRIDAY)
long elements (57 more zero bits on the left)= 21 = 0b0010101
set.contains(FRIDAY) is (elements & (1L << 4)) != 0 → true
size() is Long.bitCount(elements) = 3. add and remove are one OR / AND-NOT. no hashing, no objects.
toggle days. the whole set is the one 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".

EnumMap vals (index = ordinal)
gym
0
·
1
swim
2
·
3
·
4
·
5
·
6
EnumMap<Day, String> after put(MONDAY, "gym") and put(WEDNESDAY, "swim"): one array slot per constant, the key is the index.

both are faster and smaller than HashSet / HashMap for enum keys, and iterate in declaration order. use them. more in sets.

keylines
  • an enum is a final class with a fixed set of singleton instances.
  • each constant is a public static final field, 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().