Module 06 - Enums
Phase: Fundamentals Build tool: Maven Java: 21
Table of Contents
- Why Enums Exist
- Basic Enum Declaration
- Built-in Enum Methods
- Enums with Fields & Constructors
- Abstract Methods - Constant-Specific Behaviour
- Enums Implementing Interfaces
- Strategy Pattern via Enum
- Enum as a State Machine
- Enum in switch Expressions
- EnumSet - Efficient Set for Enums
- EnumMap - Efficient Map with Enum Keys
- Enum Singleton Pattern
- Practical Exercise - Order Management System
- Exercises
1. Why Enums Exist
Before enums (Java < 5), developers used int constants to represent a fixed set of values:
// The old way - "int enum" anti-pattern
public static final int SEASON_SPRING = 0;
public static final int SEASON_SUMMER = 1;
public static final int SEASON_AUTUMN = 2;
public static final int SEASON_WINTER = 3;
void process(int season) {
if (season == SEASON_SPRING) { ... }
}
Problems with int constants:
┌────────────────────────────────────────────────────────────────┐
│ Problem 1: Not type-safe │
│ process(42); ← compiler accepts this - 42 is a valid int │
│ process(PAYMENT_CREDIT); ← wrong domain, no compile error │
│ │
│ Problem 2: No namespace │
│ Constants from different types can collide by value │
│ │
│ Problem 3: No methods or behaviour │
│ Can't attach methods to an int constant │
│ │
│ Problem 4: Fragile printing │
│ Printing '1' tells you nothing about what it means │
│ │
│ Problem 5: switch is not exhaustive │
│ Compiler won't warn if you miss a constant │
└────────────────────────────────────────────────────────────────┘
Java enum solves all of these:
// The right way
enum Season { SPRING, SUMMER, AUTUMN, WINTER }
void process(Season season) { ... }
process(42); // COMPILE ERROR - not a Season
process(Season.SPRING); // correct
Key insight: An enum in Java is a full class. Each constant is a public static final instance of that class. This means enums can have fields, constructors, methods, and can implement interfaces.
2. Basic Enum Declaration
enum Direction {
NORTH, SOUTH, EAST, WEST // constants - each is a Direction instance
}
What the compiler generates (simplified):
final class Direction extends Enum<Direction> {
public static final Direction NORTH = new Direction("NORTH", 0);
public static final Direction SOUTH = new Direction("SOUTH", 1);
public static final Direction EAST = new Direction("EAST", 2);
public static final Direction WEST = new Direction("WEST", 3);
// generated by the compiler:
public static Direction[] values() { ... }
public static Direction valueOf(String) { ... }
}
Enum is a Singleton Per Constant
Each constant is instantiated exactly once by the JVM. This means:
Direction a = Direction.NORTH;
Direction b = Direction.NORTH;
System.out.println(a == b); // true - same instance
System.out.println(a.equals(b)); // true - equals() checks same instance
Rule: Always use
==to compare enum values. It is safe, fast, and semantically correct..equals()works too but is unnecessary.
3. Built-in Enum Methods
Every enum automatically inherits these from java.lang.Enum:
enum Planet { MERCURY, VENUS, EARTH, MARS }
Planet p = Planet.EARTH;
// name() - the exact constant name as declared in source code
p.name() // "EARTH" (String)
// ordinal() - 0-based position in declaration order
p.ordinal() // 2
// toString() - defaults to name(), can be overridden
p.toString() // "EARTH"
// compareTo() - compares by ordinal
Planet.EARTH.compareTo(Planet.MARS) // negative (EARTH comes before MARS)
// Static methods - generated by compiler
Planet.values() // Planet[] { MERCURY, VENUS, EARTH, MARS }
Planet.valueOf("MARS") // Planet.MARS - throws IllegalArgumentException if not found
Ordinals:
MERCURY VENUS EARTH MARS
0 1 2 3
Warning: ordinal() is fragile - it changes if you insert a new constant.
Never persist ordinal values to a database or file.
Use name() or a dedicated field for stable persistence.
Iterating All Constants
// values() returns a new array every call - cache it if used in a loop
for (Planet planet : Planet.values()) {
System.out.println(planet.ordinal() + ": " + planet.name());
}
4. Enums with Fields & Constructors
Enums can have fields, a constructor, and methods - making each constant carry its own data.
enum Planet {
// Each constant calls the constructor with its own values
MERCURY (3.303e+23, 2.4397e6),
VENUS (4.869e+24, 6.0518e6),
EARTH (5.976e+24, 6.37814e6),
MARS (6.421e+23, 3.3972e6);
private final double mass; // kg
private final double radius; // metres
// Constructor is ALWAYS implicitly private - you can't call it from outside
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
}
// Computed property method
double surfaceGravity() {
final double G = 6.67300E-11;
return G * mass / (radius * radius);
}
double surfaceWeight(double otherMass) {
return otherMass * surfaceGravity();
}
}
double earthWeight = 75.0;
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values()) {
System.out.printf("Weight on %-8s = %.2f N%n", p, p.surfaceWeight(mass));
}
// Weight on MERCURY = 28.33 N
// Weight on EARTH = 75.00 N
// Weight on MARS = 28.45 N
Multiple Fields
enum HttpStatus {
OK (200, "OK"),
CREATED (201, "Created"),
BAD_REQUEST (400, "Bad Request"),
UNAUTHORIZED (401, "Unauthorized"),
NOT_FOUND (404, "Not Found"),
INTERNAL_ERROR (500, "Internal Server Error");
private final int code;
private final String reason;
HttpStatus(int code, String reason) {
this.code = code;
this.reason = reason;
}
public int code() { return code; }
public String reason() { return reason; }
@Override
public String toString() { return code + " " + reason; }
// Reverse lookup: find HttpStatus by numeric code
public static HttpStatus fromCode(int code) {
for (HttpStatus s : values()) {
if (s.code == code) return s;
}
throw new IllegalArgumentException("Unknown HTTP status code: " + code);
}
}
5. Abstract Methods - Constant-Specific Behaviour
Each enum constant can provide its own implementation of an abstract method. This is like having a small class hierarchy compressed into a single enum.
enum Operation {
ADD("+") {
@Override
public double apply(double x, double y) { return x + y; }
},
SUBTRACT("-") {
@Override
public double apply(double x, double y) { return x - y; }
},
MULTIPLY("*") {
@Override
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
@Override
public double apply(double x, double y) {
if (y == 0) throw new ArithmeticException("Division by zero");
return x / y;
}
};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
// Abstract method - every constant MUST provide its own implementation
public abstract double apply(double x, double y);
@Override
public String toString() { return symbol; }
}
double x = 10, y = 3;
for (Operation op : Operation.values()) {
System.out.printf("%.1f %s %.1f = %.2f%n", x, op, y, op.apply(x, y));
}
// 10.0 + 3.0 = 13.00
// 10.0 - 3.0 = 7.00
// 10.0 * 3.0 = 30.00
// 10.0 / 3.0 = 3.33
How constant-specific bodies work internally:
Each constant that has a body is compiled as an anonymous subclass:
DIVIDE("/") { ... }
→ new Operation("/") { public double apply(...) { ... } }
This is why enum constants with bodies appear to "extend" the enum -
they're anonymous subclasses of the enum class itself.
6. Enums Implementing Interfaces
An enum can implement one or more interfaces. This allows enums to be used wherever the interface is expected - great for dependency injection and testing.
interface Describable {
String describe();
}
enum Season implements Describable {
SPRING {
@Override public String describe() { return "Warm and blooming"; }
},
SUMMER {
@Override public String describe() { return "Hot and bright"; }
},
AUTUMN {
@Override public String describe() { return "Cool and colourful"; }
},
WINTER {
@Override public String describe() { return "Cold and bare"; }
};
}
// Now Season can be used wherever Describable is expected:
void printDescription(Describable d) {
System.out.println(d.describe());
}
printDescription(Season.SPRING); // "Warm and blooming"
7. Strategy Pattern via Enum
The Strategy pattern defines a family of algorithms, encapsulates each, and makes them interchangeable. Enums are an elegant fit - no class hierarchy, no factories, no boilerplate.
enum DiscountStrategy {
NONE {
@Override public double apply(double price) { return price; }
},
STUDENT {
@Override public double apply(double price) { return price * 0.80; } // 20% off
},
SENIOR {
@Override public double apply(double price) { return price * 0.75; } // 25% off
},
EMPLOYEE {
@Override public double apply(double price) { return price * 0.60; } // 40% off
},
FLASH_SALE {
@Override public double apply(double price) { return price * 0.50; } // 50% off
};
public abstract double apply(double price);
}
// Usage - algorithm is swappable at runtime:
DiscountStrategy strategy = getStrategyForUser(user); // returns one of the constants
double finalPrice = strategy.apply(originalPrice);
Classic Strategy (class-based): Enum Strategy:
┌──────────────────────────┐ ┌─────────────────────────┐
│ «interface» │ │ enum DiscountStrategy │
│ DiscountStrategy │ │ NONE { apply()... } │
└──────────────────────────┘ │ STUDENT { apply()... } │
▲ ▲ ▲ │ SENIOR { apply()... } │
┌─────┴──┐ ┌─────┴──┐ ┌───┴────┐ │ EMPLOYEE{ apply()... } │
│ None │ │Student │ │ Senior │ └─────────────────────────┘
└────────┘ └────────┘ └────────┘
3 classes + 1 interface 1 enum - much less code
8. Enum as a State Machine
Each constant can declare which transitions are valid, making enum a self-documenting state machine.
enum OrderStatus {
PLACED {
@Override public Set<OrderStatus> allowedTransitions() {
return EnumSet.of(CONFIRMED, CANCELLED);
}
},
CONFIRMED {
@Override public Set<OrderStatus> allowedTransitions() {
return EnumSet.of(SHIPPED, CANCELLED);
}
},
SHIPPED {
@Override public Set<OrderStatus> allowedTransitions() {
return EnumSet.of(DELIVERED);
}
},
DELIVERED {
@Override public Set<OrderStatus> allowedTransitions() {
return EnumSet.noneOf(OrderStatus.class); // terminal state
}
},
CANCELLED {
@Override public Set<OrderStatus> allowedTransitions() {
return EnumSet.noneOf(OrderStatus.class); // terminal state
}
};
public abstract Set<OrderStatus> allowedTransitions();
public OrderStatus transitionTo(OrderStatus next) {
if (!allowedTransitions().contains(next))
throw new IllegalStateException(
"Cannot transition from " + this + " to " + next);
return next;
}
}
State transition diagram:
┌─────────┐ confirm ┌───────────┐ ship ┌─────────┐
│ PLACED │ ───────────► │ CONFIRMED │ ──────────► │ SHIPPED │
└────┬────┘ └─────┬─────┘ └────┬────┘
│ cancel │ cancel │ deliver
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ CANCELLED │ (terminal) │ CANCELLED │ (terminal)│ DELIVERED │ (terminal)
└───────────┘ └───────────┘ └───────────┘
9. Enum in switch Expressions
Enum + switch expression is exhaustive - the compiler knows all possible constants and warns if any are missing.
Season season = Season.SUMMER;
// Arrow switch - no fall-through, returns a value
String activity = switch (season) {
case SPRING -> "Plant a garden";
case SUMMER -> "Go to the beach";
case AUTUMN -> "Collect leaves";
case WINTER -> "Build a snowman";
// No default needed - all Season constants are covered
// Adding a new constant to Season will cause a compile error here
};
Exhaustiveness is enforced by the compiler:
┌────────────────────────────────────────────────────────────────┐
│ enum Coin { PENNY, NICKEL, DIME, QUARTER } │
│ │
│ int value = switch (coin) { │
│ case PENNY -> 1; │
│ case NICKEL -> 5; │
│ case DIME -> 10; │
│ // QUARTER missing → COMPILE ERROR: switch not exhaustive │
│ }; │
└────────────────────────────────────────────────────────────────┘
10. EnumSet - Efficient Set for Enums
EnumSet is a Set implementation backed by a bit vector (typically a single long). It is dramatically faster than HashSet for enum values.
enum Permission { READ, WRITE, DELETE, ADMIN }
// Creating EnumSets
EnumSet<Permission> none = EnumSet.noneOf(Permission.class);
EnumSet<Permission> all = EnumSet.allOf(Permission.class);
EnumSet<Permission> readOnly = EnumSet.of(Permission.READ);
EnumSet<Permission> userPerms = EnumSet.of(Permission.READ, Permission.WRITE);
// Range (by ordinal order of declaration)
EnumSet<Permission> basic = EnumSet.range(Permission.READ, Permission.DELETE);
// { READ, WRITE, DELETE }
// Complement - all constants NOT in the set
EnumSet<Permission> nonAdmin = EnumSet.complementOf(EnumSet.of(Permission.ADMIN));
// { READ, WRITE, DELETE }
How EnumSet is Implemented
Permission ordinals: READ=0 WRITE=1 DELETE=2 ADMIN=3
EnumSet.of(READ, WRITE) is stored as a single long bitmask:
┌────────────────────────────────────────────────────────────┐
│ bit 0 (READ) = 1 │
│ bit 1 (WRITE) = 1 │
│ bit 2 (DELETE) = 0 │
│ bit 3 (ADMIN) = 0 │
│ │
│ bitmask = 0b...0011 = 3 │
│ │
│ contains(READ) → (3 & (1 << 0)) != 0 → O(1) │
│ add(DELETE) → 3 | (1 << 2) = 7 → O(1) │
│ remove(WRITE) → 3 & ~(1 << 1) = 1 → O(1) │
└────────────────────────────────────────────────────────────┘
vs HashSet: hash computation + possible collision → slower
11. EnumMap - Efficient Map with Enum Keys
EnumMap is backed by a plain array indexed by ordinal. No hashing, no collision handling - faster than HashMap for enum keys.
EnumMap<Season, String> activities = new EnumMap<>(Season.class);
activities.put(Season.SPRING, "Gardening");
activities.put(Season.SUMMER, "Swimming");
activities.put(Season.AUTUMN, "Hiking");
activities.put(Season.WINTER, "Skiing");
// Iteration is always in enum declaration order (not insertion order like HashMap)
activities.forEach((season, activity) ->
System.out.println(season + " → " + activity));
EnumMap internal layout (array indexed by ordinal):
SPRING=0 SUMMER=1 AUTUMN=2 WINTER=3
┌──────────┬──────────┬──────────┬──────────┐
│Gardening │Swimming │Hiking │Skiing │
└──────────┴──────────┴──────────┴──────────┘
get(AUTUMN) → array[2] → "Hiking" - O(1) array access, no hashing
12. Enum Singleton Pattern
The safest way to implement a Singleton in Java is with a single-constant enum. It is guaranteed by the JVM to be instantiated exactly once, and it is immune to reflection attacks and serialization issues.
enum AppConfig {
INSTANCE; // the single instance
private final String dbUrl = System.getenv().getOrDefault("DB_URL", "localhost:5432");
private final int timeout = 30;
public String getDbUrl() { return dbUrl; }
public int getTimeout() { return timeout; }
}
// Usage
String url = AppConfig.INSTANCE.getDbUrl();
Why enum singleton is better than double-checked locking:
┌─────────────────────────────────────────────────────────────────┐
│ Thread safety: JVM class loading is thread-safe - no locks │
│ Serialization: Enum handles serialization automatically │
│ Reflection: Cannot create a second instance via reflection │
│ Simplicity: 3 lines vs 20 lines for double-checked locking │
└─────────────────────────────────────────────────────────────────┘
13. Practical Exercise
Files in This Module
| File | What it demonstrates |
|---|---|
EnumBasics.java | Declaration, built-in methods, fields, constructors, reverse lookup |
EnumBehavior.java | Abstract methods, interface implementation, strategy pattern |
EnumCollections.java | EnumSet, EnumMap, performance comparison |
OrderProcessor.java | Practical exercise - state machine + strategy + EnumSet/EnumMap |
OrderProcessor - What it Does
An order processing pipeline that:
- Uses
OrderStatusenum as a state machine with validated transitions - Uses
PaymentMethodenum with abstract method to compute transaction fees - Uses
PrioritywithEnumSetto filter high-priority orders - Uses
CategorywithEnumMapto aggregate revenue per product category - Uses
switchexpression exhaustively for all enum dispatch
Run:
cd module-06-enums
mvn compile exec:java -Dexec.mainClass="com.javatraining.enums.OrderProcessor"
Test:
mvn test
14. Exercises
1. Planet calculator Extend the Planet enum from Section 4 to add JUPITER, SATURN, URANUS, NEPTUNE with real mass and radius values. Add a method atmosphericPressure() that returns a relative value. Update the switch expression accordingly.
2. Day type Create a DayOfWeek enum that returns whether a day is a WEEKDAY or WEEKEND using a method, not a switch. Then use EnumSet to get all weekdays and all weekends separately.
3. State machine extension Extend the OrderStatus state machine from Section 8 to add a REFUND_REQUESTED state that can transition from DELIVERED, and a REFUNDED terminal state. Add a isTerminal() method.
4. Discount stacking Extend DiscountStrategy to support stacking two strategies. Add a static factory stacked(DiscountStrategy a, DiscountStrategy b) that returns a new strategy where both discounts are applied in sequence. Use composition, not a new constant.
5. EnumMap frequency counter Given a List<Season>, write a method that returns an EnumMap<Season, Long> counting how many times each season appears, maintaining declaration order.