Enums with Fields and Methods
Give enum constants their own data and behaviour, look them up by external codes, and avoid shared mutable state.
enum Name {
A(1), B(2);
private final int code;
Name(int code) { this.code = code; }
int code() { return code; }
}An enum is a class, so it can have fields, constructors, methods and interfaces. That lets each constant carry the data that belongs to it, instead of scattering that data across switch statements in different files.
enum HttpStatus {
OK(200, "OK"),
NOT_FOUND(404, "Not Found");
private final int code;
private final String reason;
HttpStatus(int code, String reason) {
this.code = code;
this.reason = reason;
}
}
The arguments after each constant are passed to the constructor. Enum constructors are always private, whether or not you write the keyword, because only the constants may call them. The constants come first, and the list ends with a semicolon when anything follows.
Lookup by code
Databases and partner APIs usually hold a short code, not your Java name. Give the enum a code field and a static lookup method backed by a map built once in a static initialiser placed after the constants. You cannot fill that map from the constructor: the compiler rejects references to non-constant static fields from an enum constructor, because those fields are not yet initialised while the constants are being created. Return an Optional or throw a clear exception for unknown codes.
Behaviour per constant
When each constant needs different logic, declare an abstract method and give each constant a body. The compiler then forces every new constant to implement it. For small differences, a lambda passed to the constructor and stored in a final field reads better.
Interfaces
An enum can implement an interface. A common back-end use is a catalogue of API error codes: each constant knows its HTTP status, a stable machine-readable code and a default message, and the error handler depends only on the interface, so each module can define its own error enum.
Mistakes to avoid
- Mutable fields. There is one instance per constant for the whole JVM, so a non-final field on an enum is global state shared by every request thread. Keep fields final and their values immutable.
- Calling
values()in a hot path. It returns a fresh copy of the array each time; keep an unmodifiable list in a static field if you iterate often. - Putting configuration in an enum. Prices and feature flags change without a release, so they belong in configuration or the database, with the enum only naming the kinds.
- Relying on
toString()for the constant's name. It returns the name by default, but it can be overridden for display. Usename()when you need the exact identifier.
Example
When to use it
- Map a payment provider's status codes such as "AUTH" or "SETTLED" to an enum with a static lookup by code.
- Define API error codes as enum constants that carry their HTTP status and message for a central error handler.
- Attach a pricing or routing rule to each shipping method so adding a method forces someone to write its rule.
More examples
Behaviour per constant
The abstract method means a new shipping option cannot compile without a price rule. The last line shows that a constant with a body is a subclass, which is why getDeclaringClass exists.
enum Shipping {
STANDARD {
long costPence(int grams) {
return 399;
}
},
EXPRESS {
long costPence(int grams) {
return 799 + (grams / 500) * 150;
}
},
COLLECT {
long costPence(int grams) {
return 0;
}
};
abstract long costPence(int grams);
}
void main() {
for (Shipping s : Shipping.values()) {
IO.println(s + " " + s.costPence(1200));
}
IO.println(Shipping.EXPRESS.getDeclaringClass().getSimpleName());
IO.println(Shipping.EXPRESS.getClass() == Shipping.class);
}
// Output:
// STANDARD 399
// EXPRESS 1099
// COLLECT 0
// Shipping
// falseAn API error catalogue behind an interface
The error handler only knows ErrorCode, so a payments module can add its own enum without touching it. The code strings are the stable contract for clients; the Java constant names can be refactored freely.
interface ErrorCode {
int status();
String code();
String message();
}
enum OrderError implements ErrorCode {
NOT_FOUND(404, "order.not_found", "Order not found"),
ALREADY_SHIPPED(409, "order.already_shipped", "Order has already shipped"),
INVALID_QUANTITY(400, "order.invalid_quantity", "Quantity must be between 1 and 99");
private final int status;
private final String code;
private final String message;
OrderError(int status, String code, String message) {
this.status = status;
this.code = code;
this.message = message;
}
@Override
public int status() {
return status;
}
@Override
public String code() {
return code;
}
@Override
public String message() {
return message;
}
}
String toJson(ErrorCode error) {
return "{\"status\":%d,\"code\":\"%s\",\"message\":\"%s\"}"
.formatted(error.status(), error.code(), error.message());
}
void main() {
IO.println(toJson(OrderError.ALREADY_SHIPPED));
IO.println(toJson(OrderError.INVALID_QUANTITY));
}
// Output:
// {"status":409,"code":"order.already_shipped","message":"Order has already shipped"}
// {"status":400,"code":"order.invalid_quantity","message":"Quantity must be between 1 and 99"}Caching values() and storing a lambda
Each values() call allocates a new array, while the cached list is created once and cannot be modified. A lambda field keeps short per-constant logic on one line instead of a full constant body.
enum Unit {
BYTES(n -> n),
KIB(n -> n * 1024),
MIB(n -> n * 1024 * 1024);
private static final List<Unit> ALL = List.of(values());
private final LongUnaryOperator converter;
Unit(LongUnaryOperator converter) {
this.converter = converter;
}
long toBytes(long amount) {
return converter.applyAsLong(amount);
}
static List<Unit> all() {
return ALL;
}
}
void main() {
IO.println(Unit.values() == Unit.values());
IO.println(Unit.all() == Unit.all());
for (Unit u : Unit.all()) {
IO.println("5 " + u + " = " + u.toBytes(5) + " bytes");
}
}
// Output:
// false
// true
// 5 BYTES = 5 bytes
// 5 KIB = 5120 bytes
// 5 MIB = 5242880 bytes
Discussion