Enums
Replace string and int constants with enums, use EnumSet and EnumMap, and handle enum values safely in requests and databases.
enum OrderStatus { PENDING, PAID, SHIPPED, CANCELLED }
OrderStatus s = OrderStatus.valueOf("PAID"); // s.name(), s.ordinal(), OrderStatus.values()An enum is a type with a fixed, named set of instances. enum OrderStatus { PENDING, PAID, SHIPPED, CANCELLED } creates exactly four objects when the class is initialised, and no code can create a fifth. Compare that with passing statuses around as strings: a typo such as "PIAD" compiles and fails at runtime, while OrderStatus.PIAD does not compile at all.
What every enum gets
values()returns all constants in declaration order, as a new array on each call.valueOf(String)returns the constant with exactly that name and throwsIllegalArgumentExceptionotherwise. It is case-sensitive.name()returns the declared name;ordinal()returns the zero-based position.compareToorders constants by declaration. Because each constant is a single instance, comparing with==is correct and cannot throw on null.
Enums work in switch. A switch expression that covers every constant needs no default, and when someone adds a constant the compiler reports every switch expression that does not handle it. That check is the main reason to leave default out of enum switches.
String label = switch (status) {
case PENDING -> "Awaiting payment";
case PAID -> "Paid";
case SHIPPED -> "On its way";
case CANCELLED -> "Cancelled";
};
EnumSet and EnumMap
For sets and maps keyed by an enum, use EnumSet and EnumMap. They are backed by bit vectors and arrays, so they are faster and smaller than the hash-based versions, and they iterate in declaration order, which keeps logs and JSON output stable.
Enums at the edges of a service
Requests arrive as text. Calling valueOf on a raw query parameter throws on "paid" or "PAID ", and the exception message names your internal class, so an unhandled call becomes a 500 that leaks implementation details. Normalise the input, catch the exception (or look the value up in a map) and answer 400 with the allowed values.
In the database, never store ordinal(). Insert a constant in the middle and every existing row silently points at a different status. Store name(), or an explicit code field as in the next lesson. JPA's @Enumerated defaults to EnumType.ORDINAL, so set EnumType.STRING explicitly.
Renaming a constant is a breaking change for anything stored or sent to clients by name. Treat enum names that leave the process as part of your public contract, and expect clients to receive values they do not know yet when you add one.
Example
When to use it
- Represent an order's lifecycle as an enum so a switch expression forces every handler to cover each status.
- Parse a ?status=paid query parameter into an enum and answer 400 listing the allowed values when it does not match.
- Hold a role's permissions in an EnumSet for fast, allocation-light checks on every request.
More examples
Parsing a status from a query parameter
Normalising before valueOf accepts the casing and spacing clients actually send, and unknown values become a 400 that lists the allowed ones. The last line shows the raw message, which exposes the class name and should never reach a client.
enum OrderStatus { PENDING, PAID, SHIPPED, CANCELLED }
Optional<OrderStatus> parseStatus(String raw) {
if (raw == null) {
return Optional.empty();
}
try {
return Optional.of(OrderStatus.valueOf(raw.strip().toUpperCase(Locale.ROOT)));
} catch (IllegalArgumentException e) {
return Optional.empty();
}
}
void main() {
for (String raw : List.of("paid", " Shipped ", "lost")) {
String reply = parseStatus(raw)
.map(s -> "200 filter by " + s)
.orElse("400 status must be one of " + Arrays.toString(OrderStatus.values()));
IO.println(reply);
}
try {
OrderStatus.valueOf("paid");
} catch (IllegalArgumentException e) {
IO.println(e.getMessage());
}
}
// Output:
// 200 filter by PAID
// 200 filter by SHIPPED
// 400 status must be one of [PENDING, PAID, SHIPPED, CANCELLED]
// No enum constant Main.OrderStatus.paidEnumSet for permissions, EnumMap for counts
Both collections iterate in declaration order regardless of insertion order, so the output is stable. EnumSet.complementOf gives the permissions a role lacks, which is useful in an admin screen.
enum Permission { READ, WRITE, DELETE, ADMIN }
enum OrderStatus { PENDING, PAID, SHIPPED, CANCELLED }
void main() {
Set<Permission> editor = EnumSet.of(Permission.WRITE, Permission.READ);
IO.println(editor);
IO.println(editor.contains(Permission.DELETE));
IO.println(EnumSet.complementOf(EnumSet.copyOf(editor)));
List<OrderStatus> orders = List.of(
OrderStatus.SHIPPED, OrderStatus.PAID, OrderStatus.SHIPPED, OrderStatus.PENDING);
Map<OrderStatus, Integer> counts = new EnumMap<>(OrderStatus.class);
for (OrderStatus s : orders) {
counts.merge(s, 1, Integer::sum);
}
IO.println(counts);
}
// Output:
// [READ, WRITE]
// false
// [DELETE, ADMIN]
// {PENDING=1, PAID=1, SHIPPED=2}Why ordinals must not be stored
Every paid order in the table would now read as authorised, with no error anywhere. Storing the name, or an explicit code, survives reordering and inserting constants.
// The enum as it was when rows were written...
enum StatusV1 { PENDING, PAID, SHIPPED }
// ...and after someone added a constant in the middle.
enum StatusV2 { PENDING, AUTHORISED, PAID, SHIPPED }
void main() {
StatusV1 written = StatusV1.PAID;
int storedOrdinal = written.ordinal();
String storedName = written.name();
IO.println("stored ordinal " + storedOrdinal + " reads back as " + StatusV2.values()[storedOrdinal]);
IO.println("stored name " + storedName + " reads back as " + StatusV2.valueOf(storedName));
}
// Output:
// stored ordinal 1 reads back as AUTHORISED
// stored name PAID reads back as PAID
Discussion