Java EnumMap and EnumSet: Optimized Collections for Enums

When your Map keys or Set elements are enum constants, EnumMap and EnumSet are the optimal choices: significantly faster and more memory-efficient than HashMap and HashSet. EnumMap uses an ordinal-indexed array; EnumSet uses a single long as a bit vector (for enums with up to 64 constants). Both are part of java.util and have been available since Java 5.

Contents

EnumMap: Ordinal-Indexed Map

EnumMap stores values in an array indexed by each enum constant's ordinal. No hashing, no collisions: lookups and insertions are O(1) with minimal overhead.

Java
import java.util.EnumMap;

enum Day { MON, TUE, WED, THU, FRI, SAT, SUN }

// Create and populate an EnumMap
EnumMap<Day, Integer> workHours = new EnumMap<>(Day.class);
workHours.put(Day.MON, 8);
workHours.put(Day.TUE, 8);
workHours.put(Day.WED, 8);
workHours.put(Day.THU, 8);
workHours.put(Day.FRI, 6);
workHours.put(Day.SAT, 0);
workHours.put(Day.SUN, 0);

System.out.println(workHours.get(Day.FRI)); // 6

// EnumMap iteration is always in enum declaration order (MON → SUN)
workHours.forEach((day, hours) ->
    System.out.printf("%s: %d hours%n", day, hours));

// Copy from a regular map
Map<Day, String> dayNames = new HashMap<>();
dayNames.put(Day.MON, "Monday");
dayNames.put(Day.TUE, "Tuesday");
EnumMap<Day, String> enumDayNames = new EnumMap<>(dayNames); // copy constructor

// putIfAbsent, computeIfAbsent — all Map methods work
workHours.putIfAbsent(Day.SAT, 4);
workHours.merge(Day.FRI, 2, Integer::sum); // FRI → 8

// getOrDefault
int hours = workHours.getOrDefault(Day.SUN, 0);

EnumSet: Bit-Vector Set

EnumSet represents a set of enum values as a single long bit vector (for enums ≤ 64 values) or a long[] array. Set operations become bitwise operations: extremely fast.

Java
import java.util.EnumSet;

enum Permission { READ, WRITE, EXECUTE, DELETE, ADMIN }

// Creating EnumSets
EnumSet<Permission> empty      = EnumSet.noneOf(Permission.class);
EnumSet<Permission> all        = EnumSet.allOf(Permission.class);
EnumSet<Permission> readWrite  = EnumSet.of(Permission.READ, Permission.WRITE);
EnumSet<Permission> readOnly   = EnumSet.of(Permission.READ);

// range — all constants between two (by declaration order)
EnumSet<Permission> firstThree = EnumSet.range(Permission.READ, Permission.EXECUTE);
// {READ, WRITE, EXECUTE}

// complementOf — all constants NOT in the given set
EnumSet<Permission> notReadWrite = EnumSet.complementOf(readWrite);
// {EXECUTE, DELETE, ADMIN}

// copyOf — copy an existing EnumSet
EnumSet<Permission> copy = EnumSet.copyOf(readWrite);

// Standard Set operations — all very fast (bitwise internally)
readWrite.add(Permission.EXECUTE);               // add
readWrite.remove(Permission.WRITE);              // remove
boolean canRead = readWrite.contains(Permission.READ); // true

// Set algebra
EnumSet<Permission> union = EnumSet.copyOf(readWrite);
union.addAll(readOnly);      // union

EnumSet<Permission> intersection = EnumSet.copyOf(readWrite);
intersection.retainAll(readOnly); // intersection

EnumSet<Permission> difference = EnumSet.copyOf(readWrite);
difference.removeAll(readOnly);   // difference

EnumSet Factory Methods

EnumSet has no public constructor: instances are always created through static factory methods. The factories cover all common patterns: empty set, full set, explicit members, a contiguous range by declaration order, and the complement of an existing set:

Java
enum Status { PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED, RETURNED }

// noneOf — empty set of the given enum type
EnumSet<Status> noStatuses = EnumSet.noneOf(Status.class);

// allOf — set containing all constants
EnumSet<Status> allStatuses = EnumSet.allOf(Status.class);

// of — 1 to 5 args, or varargs (6+ args use the varargs form)
EnumSet<Status> active  = EnumSet.of(Status.PENDING, Status.PROCESSING, Status.SHIPPED);
EnumSet<Status> terminal = EnumSet.of(Status.DELIVERED, Status.CANCELLED, Status.RETURNED);

// range — inclusive on both ends, uses enum declaration order
EnumSet<Status> inFlight = EnumSet.range(Status.PROCESSING, Status.SHIPPED);
// {PROCESSING, SHIPPED}

// complementOf — everything else
EnumSet<Status> notActive = EnumSet.complementOf(active);
// {DELIVERED, CANCELLED, RETURNED}

// Using as flags — idiomatic pattern
boolean isTerminal(Status status) {
    return terminal.contains(status);
}

boolean isActive(Status status) {
    return active.contains(status);
}

// Method accepting Set<Status> — use EnumSet for the argument
void processOrders(EnumSet<Status> statusFilter) {
    orders.stream()
        .filter(o -> statusFilter.contains(o.status()))
        .forEach(this::process);
}
// Call: processOrders(EnumSet.of(Status.PENDING, Status.PROCESSING));

Performance Characteristics

EnumMap is faster than HashMap because it uses an ordinal-indexed array: no hashing, no collision resolution. EnumSet is even more dramatic: for enums with up to 64 constants the entire set is a single long, so operations like contains() reduce to a single bitwise test:

Java
// EnumMap vs HashMap — benchmark illustration (not runnable)
// EnumMap: no hash computation, no collision handling
// HashMap:  hash(key) → bucket lookup → possible chain/tree traversal

// EnumSet vs HashSet — even bigger difference
// EnumSet.contains(x): single bit test — O(1), ~1 CPU instruction
// HashSet.contains(x): hash, bucket, equals — O(1) amortized but higher constant

// Memory comparison for an enum with 8 constants, all in the collection:
// EnumSet:   1 long (8 bytes) for the bit vector
// HashSet:   8 Entry objects (~32+ bytes each) + array + header

// EnumSet for flags — replaces int bitmasks cleanly:
// Old (pre-Java-5 style, error-prone):
// static final int READ    = 1 << 0;
// static final int WRITE   = 1 << 1;
// static final int EXECUTE = 1 << 2;
// int perms = READ | WRITE;
// if ((perms & READ) != 0) { ... }

// Modern (type-safe, readable, same performance):
EnumSet<Permission> perms = EnumSet.of(Permission.READ, Permission.WRITE);
if (perms.contains(Permission.READ)) { /* ... */ }

// Iteration order:
// EnumMap — always enum declaration order (consistent, predictable)
// EnumSet — always enum declaration order
// HashMap — unspecified (insertion or hash order)
// LinkedHashMap — insertion order
// TreeMap — natural/comparator order

Practical Patterns

Three patterns appear repeatedly in production code: feature-flag configuration (EnumMap<Feature, Boolean>), strategy dispatch (EnumMap<Op, Function> replacing switch statements), and role-based access control (EnumMap<Role, EnumSet<Permission>>):

Java
// Pattern 1: Configuration with EnumMap
enum Feature { DARK_MODE, NOTIFICATIONS, AUTO_SAVE, SPELL_CHECK }

class UserPreferences {
    private final EnumMap<Feature, Boolean> enabled =
        new EnumMap<>(Feature.class);

    public UserPreferences() {
        // Default all features to false
        for (Feature f : Feature.values()) enabled.put(f, false);
    }

    public void enable(Feature feature)   { enabled.put(feature, true); }
    public void disable(Feature feature)  { enabled.put(feature, false); }
    public boolean isEnabled(Feature f)   { return enabled.getOrDefault(f, false); }

    public EnumSet<Feature> enabledFeatures() {
        return enabled.entrySet().stream()
            .filter(Map.Entry::getValue)
            .map(Map.Entry::getKey)
            .collect(Collectors.toCollection(() -> EnumSet.noneOf(Feature.class)));
    }
}

// Pattern 2: Strategy dispatch with EnumMap
enum Operation { ADD, SUBTRACT, MULTIPLY, DIVIDE }

EnumMap<Operation, IntBinaryOperator> ops = new EnumMap<>(Operation.class);
ops.put(Operation.ADD,      (a, b) -> a + b);
ops.put(Operation.SUBTRACT, (a, b) -> a - b);
ops.put(Operation.MULTIPLY, (a, b) -> a * b);
ops.put(Operation.DIVIDE,   (a, b) -> a / b);

int calculate(Operation op, int a, int b) {
    return ops.get(op).applyAsInt(a, b);
}

// Pattern 3: Role-based access control with EnumSet
enum Role  { USER, MODERATOR, ADMIN }

EnumMap<Role, EnumSet<Permission>> rolePermissions = new EnumMap<>(Role.class);
rolePermissions.put(Role.USER,      EnumSet.of(Permission.READ));
rolePermissions.put(Role.MODERATOR, EnumSet.of(Permission.READ, Permission.WRITE, Permission.DELETE));
rolePermissions.put(Role.ADMIN,     EnumSet.allOf(Permission.class));

boolean hasPermission(Role role, Permission perm) {
    return rolePermissions.getOrDefault(role, EnumSet.noneOf(Permission.class))
                          .contains(perm);
}
Always use EnumMap and EnumSet when keys/elements are enum constants. They're never the wrong choice: they are type-safe, faster, and more memory-efficient than HashMap and HashSet for this use case.