Enums, Records & Sealed Types

Advanced Enums

15 min Lesson 3 of 13

Advanced Enums

The previous two lessons introduced enum syntax and showed how to attach fields, constructors, and methods to constants. This lesson pushes further into three areas that separate intermediate Java programmers from beginners: constant-specific method bodies, and the two specialized collections EnumSet and EnumMap. Along the way you will also see how values() and valueOf() work under the hood.

Constant-Specific Method Bodies

Every enum constant can override an abstract (or concrete) method declared in the enum body. This technique is called a constant-specific class body, and it lets each constant carry its own implementation rather than forcing a giant switch statement in the caller.

Consider a simple order-processing system where each shipping carrier calculates its cost differently:

public enum Carrier { STANDARD { @Override public double shippingCost(double weightKg) { return 2.00 + weightKg * 0.50; } }, EXPRESS { @Override public double shippingCost(double weightKg) { return 5.00 + weightKg * 1.20; } }, OVERNIGHT { @Override public double shippingCost(double weightKg) { return 12.00 + weightKg * 2.50; } }; // Every constant MUST implement this because it is abstract. public abstract double shippingCost(double weightKg); }

Usage is clean — the caller never needs to know which carrier it is talking to:

Carrier carrier = Carrier.EXPRESS; double cost = carrier.shippingCost(3.5); // 5.00 + 3.5 * 1.20 = 9.20 System.out.println("Cost: " + cost);
Why not a switch? A switch on an enum inside a helper method is fragile: adding a new constant means finding and updating every switch. With constant-specific bodies, you add the constant and its implementation together — the compiler forces you to provide the method, so nothing is forgotten.

You can also override a concrete (non-abstract) method in only some constants, letting the rest fall back to the default:

public enum LogLevel { DEBUG, INFO, WARN, ERROR { @Override public void log(String message) { // Prefix every error with a visible marker. System.err.println("!!! ERROR !!! " + message); } }; // Default implementation used by DEBUG, INFO, and WARN. public void log(String message) { System.out.println("[" + name() + "] " + message); } }

values() and valueOf()

The compiler silently generates two static methods on every enum:

  • values() — returns a new array containing all constants in declaration order. Use it to iterate.
  • valueOf(String name) — finds a constant by its exact name; throws IllegalArgumentException if the name does not match.
// Iterating all constants for (Carrier c : Carrier.values()) { System.out.printf("%s: %.2f%n", c, c.shippingCost(1.0)); } // Parsing from user input / config files String input = "EXPRESS"; Carrier chosen = Carrier.valueOf(input); // returns Carrier.EXPRESS
valueOf is case-sensitive. Carrier.valueOf("express") throws IllegalArgumentException at runtime. Always normalise the input first: Carrier.valueOf(input.toUpperCase()), or wrap the call in a try-catch.

Every enum also inherits name() (returns the constant's declared name as a String) and ordinal() (returns its zero-based position). Rely on name() for display and serialisation; be careful with ordinal() because inserting a constant in the middle changes all following ordinals.

EnumSet — Efficient Bit Sets for Enum Constants

EnumSet<E extends Enum<E>> is a Set implementation optimised specifically for enum constants. Internally it uses a single long bitmask (or two for enums with more than 64 constants), making every operation — add, contains, remove — an O(1) bit manipulation instead of a hash lookup.

import java.util.EnumSet; public enum Permission { READ, WRITE, DELETE, ADMIN } // --- Creating sets --- EnumSet<Permission> readOnly = EnumSet.of(Permission.READ); EnumSet<Permission> allPerms = EnumSet.allOf(Permission.class); EnumSet<Permission> noPerms = EnumSet.noneOf(Permission.class); EnumSet<Permission> nonAdmin = EnumSet.complementOf(EnumSet.of(Permission.ADMIN)); // --- Checking permissions --- boolean canDelete = readOnly.contains(Permission.DELETE); // false boolean canRead = allPerms.contains(Permission.READ); // true // --- Bulk operations mirror Set --- noPerms.add(Permission.READ); noPerms.addAll(EnumSet.of(Permission.WRITE, Permission.DELETE)); System.out.println(noPerms); // [READ, WRITE, DELETE]
Prefer EnumSet over HashSet whenever your set elements are enum constants. It is faster, uses less memory, and its toString() output is always in declaration order, making debugging easier.

EnumMap — Ordered, Fast Map Keyed by Enum Constants

EnumMap<K extends Enum<K>, V> is the companion to EnumSet. It stores values in an internal array indexed by each constant's ordinal(), giving O(1) access with near-zero overhead and always iterating in declaration order.

import java.util.EnumMap; import java.util.Map; public enum Day { MON, TUE, WED, THU, FRI, SAT, SUN } EnumMap<Day, String> schedule = new EnumMap<>(Day.class); schedule.put(Day.MON, "Team standup at 9am"); schedule.put(Day.WED, "Design review at 2pm"); schedule.put(Day.FRI, "Sprint retro at 4pm"); // Iterates in MON → SUN order, not insertion order. for (Map.Entry<Day, String> entry : schedule.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); } // Lookup String fridayTask = schedule.getOrDefault(Day.FRI, "No meetings"); System.out.println(fridayTask); // Sprint retro at 4pm

A common real-world pattern is combining EnumMap with constant-specific method bodies. Store a strategy (a lambda or a method reference) per constant, then dispatch through the map instead of a switch:

import java.util.EnumMap; import java.util.function.DoubleUnaryOperator; enum TaxBand { BASIC, HIGHER, ADDITIONAL } EnumMap<TaxBand, DoubleUnaryOperator> taxCalc = new EnumMap<>(TaxBand.class); taxCalc.put(TaxBand.BASIC, income -> income * 0.20); taxCalc.put(TaxBand.HIGHER, income -> income * 0.40); taxCalc.put(TaxBand.ADDITIONAL, income -> income * 0.45); double tax = taxCalc.get(TaxBand.HIGHER).applyAsDouble(50_000); System.out.println("Tax owed: " + tax); // 20000.0

Putting It Together

Advanced enums are more than named constants — they are a lightweight object-oriented pattern. By combining constant-specific method bodies with EnumSet and EnumMap, you can model domain rules clearly, avoid fragile switch statements scattered across the codebase, and benefit from the performance characteristics of enum-optimised collections. In the next lesson you will meet Records, another modern Java feature that pairs naturally with enums.