View source on GitHub

Module 06 - Enums

Phase: Fundamentals Build tool: Maven Java: 21

Table of Contents

  1. Why Enums Exist
  2. Basic Enum Declaration
  3. Built-in Enum Methods
  4. Enums with Fields & Constructors
  5. Abstract Methods - Constant-Specific Behaviour
  6. Enums Implementing Interfaces
  7. Strategy Pattern via Enum
  8. Enum as a State Machine
  9. Enum in switch Expressions
  10. EnumSet - Efficient Set for Enums
  11. EnumMap - Efficient Map with Enum Keys
  12. Enum Singleton Pattern
  13. Practical Exercise - Order Management System
  14. 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 OrderStatus enum as a state machine with validated transitions
  • Uses PaymentMethod enum with abstract method to compute transaction fees
  • Uses Priority with EnumSet to filter high-priority orders
  • Uses Category with EnumMap to aggregate revenue per product category
  • Uses switch expression 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.


Next

Module 07 - OOP: Classes & Objects