JAVA / RECORDS, SEALED TYPES AND ENUMS
Enums with fields, constructors and methods
Give each enum constant its own data with fields, a constructor and methods, and know when those fields are initialized relative to static state.
What you will learn
- Pass per-constant data as constructor arguments in the parentheses after each constant
- Declare enum fields private final; the constructor is implicitly private
- Build reverse-lookup maps in a static block, not in the enum constructor
- Compare constants by reading another instance's private field, as in other.level
Understanding Enums with fields, constructors and methods
A constant written as KILO("k", 3) is not decoration: the parentheses are an argument list, and each constant is one call to the enum's constructor. The compiler turns every name in that list into a public static final field of the enum type and initializes them in declaration order the first time the class is touched. So an enum with fields is a small fixed set of pre-built objects, and the fields are where the data that differs between them lives, instead of a parallel array or a switch over the constant name.
Because the constant list is the only place instances can come from, the constructor may not be public or protected; no modifier or private are the only choices, and new Prefix("k", 3) will never compile. That closed set is what makes final fields the natural choice and lets methods read them directly: nobody can add a fourth constant, and the three that exist are the same three objects for the whole life of the JVM. The flip side is that a non-final field on a constant is global mutable state shared by every caller.
Enum constants are static fields too, they must be declared first in the body, and static initializers run in textual order, so the constants are always built before any other static field is assigned and before any static block runs. That ordering is exactly why javac refuses a reference to a static field from an enum constructor with "illegal reference to static field from initializer": at that moment the field is still null or zero. Anything that needs all the constants to already exist, such as a reverse lookup table, belongs in a static block after the constant list, where values() returns fully built objects.
enum Prefix {
KILO("k", 3),
MEGA("M", 6),
GIGA("G", 9);
private final String symbol;
private final int exponent;
// The print is here only to show when the constructor runs.
Prefix(String symbol, int exponent) {
System.out.println("building " + name());
this.symbol = symbol;
this.exponent = exponent;
}
public String symbol() {
return symbol;
}
public double scale(double value) {
return value * Math.pow(10, exponent);
}
}
public class Main {
public static void main(String[] args) {
for (Prefix p : Prefix.values()) {
System.out.printf("1 %sB = %.0f bytes%n", p.symbol(), p.scale(1));
}
}
}The constant list is a list of constructor calls, so each enum constant is one pre-built instance whose fields were set once when the enum class was initialized.
Worked examples
Lookup by field value
Finding a constant from the number it carries, using a map filled in a static block.
import java.util.HashMap;
import java.util.Map;
enum HttpStatus {
OK(200),
NOT_FOUND(404),
TEAPOT(418);
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;
}
static HttpStatus ofCode(int code) {
HttpStatus s = BY_CODE.get(code);
if (s == null) {
throw new IllegalArgumentException("unknown code: " + code);
}
return s;
}
int code() {
return code;
}
}
public class Main {
public static void main(String[] args) {
System.out.println(HttpStatus.ofCode(404));
System.out.println(HttpStatus.OK.code());
try {
HttpStatus.ofCode(500);
} catch (IllegalArgumentException e) {
System.out.println("caught: " + e.getMessage());
}
}
}Example explained
Line 1The static block runs after all three constants exist, so values() there returns a complete array; the same loop inside the constructor would not compile.
Line 2BY_CODE.put(s.code, s) reads a private instance field of a different object, which is legal because the code sits inside the same enum class.
Line 3ofCode(500) throws instead of returning null, so an unmapped number fails at the call site with a readable message.
Line 4Printing ofCode(404) uses the inherited toString(), which yields the constant name NOT_FOUND rather than the code.
Two constructors, one constant list
Overloading the enum constructor so some constants can omit an argument.
enum Retry {
NONE(0, 0),
QUICK(3),
PATIENT(5, 500);
private final int attempts;
private final int backoffMillis;
Retry(int attempts) {
this(attempts, 100);
}
Retry(int attempts, int backoffMillis) {
this.attempts = attempts;
this.backoffMillis = backoffMillis;
}
int totalWaitMillis() {
return attempts * backoffMillis;
}
}
public class Main {
public static void main(String[] args) {
for (Retry r : Retry.values()) {
System.out.println(r + " -> " + r.totalWaitMillis() + " ms");
}
}
}Example explained
Line 1QUICK(3) matches the one-argument constructor, so constants in the same list may differ in shape as long as some constructor fits.
Line 2this(attempts, 100) delegates to the other constructor, the ordinary chaining rule, which keeps the default backoff in one place.
Line 3In totalWaitMillis() the names attempts and backoffMillis resolve to the receiving constant's own fields, so each constant gets its own answer.
Line 4NONE stores 0 for both fields and returns 0 with no special case written for it.
Derived fields and comparing constants
Computing a field once in the constructor and letting a method read another constant's private field.
enum Severity {
DEBUG(10),
WARN(30),
FATAL(50);
private final int level;
private final boolean pageOnCall;
Severity(int level) {
this.level = level;
this.pageOnCall = level >= 50;
}
boolean atLeast(Severity other) {
return this.level >= other.level;
}
boolean pages() {
return pageOnCall;
}
}
public class Main {
public static void main(String[] args) {
System.out.println(Severity.WARN.atLeast(Severity.DEBUG));
System.out.println(Severity.WARN.pages());
System.out.println(Severity.FATAL.pages());
}
}Example explained
Line 1pageOnCall is derived from the constructor argument once, at class initialization, not recomputed on every pages() call.
Line 2other.level reaches into a second constant's private field: private means private to the class, not to the instance.
Line 3WARN.atLeast(DEBUG) compares 30 with 10, so the ranking comes from a field you chose rather than from declaration position.
Line 4FATAL.pages() is true because its argument 50 satisfied the condition when the constant was built.
Important notes
Each constant is a single instance for the whole JVM, so a non-final field is shared global state: a counter bumped on Severity.FATAL is visible to every caller and is not thread-safe.
name() and ordinal() already work inside the constructor body because the compiler passes them to the Enum superclass first, so never keep a field holding the constant's own name; note also that an exception thrown in an enum constructor surfaces as ExceptionInInitializerError on first use, not at your call site.
Common mistakes
Writing new Severity(50) or marking the constructor public: the first fails with "enum types may not be instantiated", the second with "modifier public not allowed here", because the constant list is the only place instances are created.
Filling a lookup map from inside the constructor: javac rejects it with "illegal reference to static field from initializer", and hiding the access behind a static helper method only moves the problem, producing a NullPointerException during class initialization because the map field is still null.
Using ordinal() as the meaningful number instead of declaring a field: inserting or reordering one constant silently shifts every value, so persisted or transmitted numbers start meaning something else.
Try it yourself
Change, predict, then run
Write an enum Direction with NORTH, EAST, SOUTH and WEST, each carrying dx and dy fields drawn from -1, 0 and 1, plus an instance method opposite() that scans values() for the constant with the negated pair. Print each constant next to its opposite and check that all four round-trip.
Open the Java workspaceCheck your understanding
An enum keeps a private static final Map that maps a code back to its constant. Why must that map be filled in a static block rather than in the enum constructor?
- Because a static block runs before the constants are created, so the map is ready in time
- Because an enum constructor may not call methods that can throw, and Map.put can throw
- Because every constant is created before any other static field is assigned, so the map is still null while the constructor runs
- Because the enum constructor is private, and private members cannot access static state
Show answer
Enum constants are declared first and static initializers run in textual order, so all constants are built before the map field gets its value; javac therefore rejects a direct reference to that field from the constructor, and an indirect one would see null. Option 1 has the order backwards: the static block is precisely the code that runs after every constant exists, which is why values() is safe to call there.