Enums
An enum (enumerated type) is a type with a fixed, known set of named values. Days of the week, order statuses, user roles, compass directions, and log levels are natural enums: the complete list of possible values is known when the code is written. Java enums are full classes. Each constant is an object, and an enum can have fields, constructors, and methods, which makes them far more capable than the integer constants used in older code.
Declaring an Enum
public enum OrderStatus {
PLACED,
PAID,
SHIPPED,
DELIVERED,
CANCELLED
}
OrderStatus status = OrderStatus.PAID;
System.out.println(status); // PAID
if (status == OrderStatus.PAID) {
System.out.println("Ready to ship");
}
Constants are written in UPPER_SNAKE_CASE by convention. A variable of type OrderStatus can hold only one of these five values, or null. Nothing else compiles.
Why Not Use int or String Constants?
Before Java 5, fixed sets of values were often written as constants:
public static final int PLACED = 0;
public static final int PAID = 1;
public static final int SHIPPED = 2;
void update(int status) { ... }
update(PAID); // intended
update(42); // also compiles: meaningless value
update(SOME_OTHER_CONSTANT_FROM_ANOTHER_CLASS); // compiles: wrong kind of value
| Problem with int/String constants | How enums solve it |
|---|---|
Any int or String is accepted, including invalid ones | Only the declared constants are valid; checked at compile time |
Printing shows 1 instead of a meaningful name | toString() returns the constant's name |
No namespace: PAID might clash with another PAID | Constants are qualified: OrderStatus.PAID |
| Cannot carry data or behavior | Enums can have fields and methods |
Typos in string constants ("SHIPED") are found only at runtime | Typos in enum names do not compile |
Comparing Enum Values
Each enum constant is a single object, created once when the enum class is loaded. Comparing with == is therefore correct and preferred:
if (status == OrderStatus.CANCELLED) { ... }
== is also null-safe: status == OrderStatus.CANCELLED is simply false when status is null, whereas status.equals(...) would throw.
Enums in switch
Enums work naturally with switch. Inside the cases, constants are written without the type prefix:
static String message(OrderStatus s) {
return switch (s) {
case PLACED -> "We have received your order.";
case PAID -> "Payment confirmed.";
case SHIPPED -> "Your order is on its way.";
case DELIVERED -> "Delivered. Enjoy!";
case CANCELLED -> "This order was cancelled.";
};
}
This switch expression has no default, yet it compiles, because it covers every constant. If a RETURNED constant is later added to the enum, the compiler reports this switch as incomplete. Leaving out default in enum switch expressions turns "forgot to handle the new status" into a compile error instead of a runtime surprise.
Built-in Methods
Every enum automatically extends java.lang.Enum and gets several methods:
| Method | Returns | Example |
|---|---|---|
name() | The constant's exact name | PAID.name() → "PAID" |
ordinal() | Its position, starting from 0 | PAID.ordinal() → 1 |
values() (static) | An array of all constants, in declaration order | OrderStatus.values() |
valueOf(String) (static) | The constant with that exact name | OrderStatus.valueOf("SHIPPED") |
compareTo(other) | Order comparison by ordinal | PAID.compareTo(SHIPPED) → negative |
toString() | The name, unless overridden | PAID.toString() → "PAID" |
for (OrderStatus s : OrderStatus.values()) {
System.out.println(s.ordinal() + " " + s);
}
OrderStatus parsed = OrderStatus.valueOf("DELIVERED");
OrderStatus bad = OrderStatus.valueOf("delivered"); // IllegalArgumentException: case-sensitive
valueOf throws IllegalArgumentException for an unknown name (and NullPointerException for null). When parsing user input or external data, normalize and handle the failure:
static OrderStatus parse(String text) {
try {
return OrderStatus.valueOf(text.strip().toUpperCase());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Unknown status: " + text);
}
}
Avoid storing ordinal() in a database or file. Inserting a new constant in the middle of the enum shifts every later ordinal and silently corrupts stored data. Store name(), or a dedicated code field.
Fields, Constructors, and Methods
Enum constants can carry data. Each constant passes arguments to the enum's constructor:
public enum Plan {
FREE(0, 1),
PRO(499, 5),
TEAM(1999, 25);
private final int monthlyPriceInr;
private final int maxProjects;
Plan(int monthlyPriceInr, int maxProjects) { // constructor is implicitly private
this.monthlyPriceInr = monthlyPriceInr;
this.maxProjects = maxProjects;
}
public int getMonthlyPriceInr() { return monthlyPriceInr; }
public int getMaxProjects() { return maxProjects; }
public int yearlyPriceInr() {
return monthlyPriceInr * 10; // two months free on yearly billing
}
public boolean allows(int projects) {
return projects <= maxProjects;
}
}
Plan p = Plan.PRO;
System.out.println(p + " costs " + p.getMonthlyPriceInr() + "/month"); // PRO costs 499/month
System.out.println("Yearly: " + p.yearlyPriceInr()); // Yearly: 4990
System.out.println(p.allows(8)); // false
Rules:
- The list of constants must come first in the enum body, ending with a semicolon when other members follow.
- Enum constructors are always
private(explicitly or implicitly). Code outside the enum cannot create new instances; the constants are the only ones that will ever exist. - Fields should be
final. Enum constants are shared across the entire program, so mutable state in them behaves like a global variable.
Constant-Specific Behavior
Each constant can provide its own implementation of a method, which replaces a switch with polymorphism:
public enum Operation {
ADD("+") {
public double apply(double a, double b) { return a + b; }
},
SUBTRACT("-") {
public double apply(double a, double b) { return a - b; }
},
MULTIPLY("×") {
public double apply(double a, double b) { return a * b; }
},
DIVIDE("÷") {
public double apply(double a, double b) {
if (b == 0) throw new ArithmeticException("division by zero");
return a / b;
}
};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
public abstract double apply(double a, double b);
public String symbol() { return symbol; }
}
for (Operation op : Operation.values()) {
System.out.println("8 " + op.symbol() + " 2 = " + op.apply(8, 2));
}
Output:
8 + 2 = 10.0
8 - 2 = 6.0
8 × 2 = 16.0
8 ÷ 2 = 4.0
The abstract method forces every constant to implement apply; adding a new operation without it does not compile. Alternatively, a constructor can take a lambda: ADD("+", (a, b) -> a + b) with a field of type DoubleBinaryOperator.
Enums Implementing Interfaces
Enums cannot extend another class (they already extend Enum), but they can implement interfaces:
public interface Labeled {
String label();
}
public enum Priority implements Labeled {
LOW, MEDIUM, HIGH, CRITICAL;
@Override
public String label() {
return name().charAt(0) + name().substring(1).toLowerCase(); // "High"
}
public boolean isUrgent() {
return this.compareTo(HIGH) >= 0; // HIGH or CRITICAL
}
}
Declaration order defines the natural ordering used by compareTo, so placing constants from lowest to highest makes comparisons like isUrgent read naturally.
EnumSet and EnumMap
The JDK includes collections optimized for enum keys and elements:
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
enum Day { MON, TUE, WED, THU, FRI, SAT, SUN }
Set<Day> weekend = EnumSet.of(Day.SAT, Day.SUN);
Set<Day> weekdays = EnumSet.complementOf(EnumSet.copyOf(weekend));
Set<Day> midweek = EnumSet.range(Day.TUE, Day.THU);
Map<Day, Integer> hoursWorked = new EnumMap<>(Day.class);
hoursWorked.put(Day.MON, 8);
hoursWorked.put(Day.TUE, 7);
System.out.println(weekdays); // [MON, TUE, WED, THU, FRI]
System.out.println(midweek); // [TUE, WED, THU]
System.out.println(hoursWorked); // {MON=8, TUE=7}
EnumSet stores membership as bits, making it extremely compact and fast; EnumMap uses an array indexed by ordinal. Both iterate in declaration order. Prefer them over HashSet and HashMap whenever the keys are enum constants.
Enums as Singletons
An enum with a single constant is the simplest thread-safe singleton in Java:
public enum AppConfig {
INSTANCE;
private final String environment = System.getProperty("app.env", "development");
public String environment() { return environment; }
}
System.out.println(AppConfig.INSTANCE.environment());
The JVM guarantees the constant is created exactly once, safely, even under concurrent access or serialization.
Practical Example: Order Workflow
import java.util.EnumSet;
import java.util.Set;
public class OrderWorkflow {
enum Status {
PLACED, PAID, SHIPPED, DELIVERED, CANCELLED;
Set<Status> next() {
return switch (this) {
case PLACED -> EnumSet.of(PAID, CANCELLED);
case PAID -> EnumSet.of(SHIPPED, CANCELLED);
case SHIPPED -> EnumSet.of(DELIVERED);
case DELIVERED, CANCELLED -> EnumSet.noneOf(Status.class);
};
}
boolean canMoveTo(Status target) {
return next().contains(target);
}
boolean isFinal() {
return next().isEmpty();
}
}
static class Order {
private final String id;
private Status status = Status.PLACED;
Order(String id) { this.id = id; }
void moveTo(Status target) {
if (!status.canMoveTo(target)) {
throw new IllegalStateException(
id + ": cannot go from " + status + " to " + target
+ " (allowed: " + status.next() + ")");
}
status = target;
System.out.println(id + " -> " + status + (status.isFinal() ? " (final)" : ""));
}
}
public static void main(String[] args) {
Order o = new Order("ORD-7");
o.moveTo(Status.PAID);
o.moveTo(Status.SHIPPED);
try {
o.moveTo(Status.CANCELLED);
} catch (IllegalStateException e) {
System.out.println(e.getMessage());
}
o.moveTo(Status.DELIVERED);
}
}
Output:
ORD-7 -> PAID
ORD-7 -> SHIPPED
ORD-7: cannot go from SHIPPED to CANCELLED (allowed: [DELIVERED])
ORD-7 -> DELIVERED (final)
The enum holds both the set of possible statuses and the rules for moving between them. next() uses an exhaustive switch on this, so adding a new status forces its transitions to be defined. EnumSet represents the allowed targets compactly. Order cannot reach an invalid state, because every change goes through canMoveTo. A design like this, a small state machine encoded in an enum, is common for orders, payments, support tickets, and approval workflows.