Skip to content

How to Use EnumMap and EnumSet in Java 21 (Rota Example)

Build an on-call rota in Java 21 with EnumSet for roles and gaps and EnumMap for schedules and counts, then trigger ten traps that bite in real code.

java-collections java-tutorials how-to-tutorials
Gopi Gorantala
Reading Progress

On This Page

What the rota prints when it's done

A small on-call rota, five days covered out of seven, plus a table of who can do what. This is the output of the finished program:

Who is on call:
  MONDAY    Priya
  TUESDAY   Priya
  WEDNESDAY Marcus
  THURSDAY  -- nobody --
  FRIDAY    Marcus
  SATURDAY  -- nobody --
  SUNDAY    Aiko
Gaps: [THURSDAY, SATURDAY]
Weekday gaps: [THURSDAY]
By engineer: {Aiko=[SUNDAY], Marcus=[WEDNESDAY, FRIDAY], Priya=[MONDAY, TUESDAY]}

VIEWER   can [VIEW], cannot [EDIT, APPROVE, DEPLOY, ADMIN]
ENGINEER can [VIEW, EDIT, DEPLOY], cannot [APPROVE, ADMIN]
LEAD     can [VIEW, EDIT, APPROVE, DEPLOY], cannot [ADMIN]
ADMIN    can [VIEW, EDIT, APPROVE, DEPLOY, ADMIN], cannot []

Incidents: {SEV1=1, SEV2=0, SEV3=3, SEV4=0}
Per day: {MONDAY=1, WEDNESDAY=2, SATURDAY=1}

Two collections do the work behind that output. An EnumSet holds "which days" and "which permissions". An EnumMap holds "who is on call each day" and "how many incidents per severity". If your code has a HashMap<DayOfWeek, ...> or a HashSet<Permission> in it, this article is for you. By the end you'll know how to use EnumMap and EnumSet in Java, why they print in a fixed order, and which ten behaviours surprise people. I forced every one of them so the messages below are real.

There are no dependencies. Eight source files, plain javac. The part that took longest was trap 4, because the exception only shows up when a list happens to be empty, and my first draft of the rota never hit it.

If you haven't used enums with fields and methods yet, read How to use Java enums with fields, methods, and abstract behaviour first. This one assumes values() and ordinal() are familiar.

Tools I used

Tool Version I used Check it with
JDK OpenJDK 21.0.12.1 (Ubuntu build) java -version
javac 21.0.12.1, also compiled with --release 17 javac -version
IntelliJ IDEA / Eclipse not run, menu paths from vendor help Help → About
Build tool none

The Incident record means you need Java 16 or newer; I verified 17 and 21. The EnumMap and EnumSet parts themselves have been around since Java 5. You should know what a generic type and a for loop are. Nothing else.

If you need a JDK, install one with SDKMAN (sdk install java 21.0.12-tem or whichever 21 build your vendor lists), or use whatever your team has standardised on.

Create the rota project by hand

I used the command line as the primary path this time, because the project is eight files and an IDE wizard would add more noise than help.

mkdir -p rota/src/main/java/com/example/rota rota/src/test/java/com/example/rota
cd rota

The tree you're building:

rota
└── src
    ├── main/java/com/example/rota
    │   ├── Permission.java
    │   ├── Role.java
    │   ├── Severity.java
    │   ├── Incident.java
    │   ├── Rota.java
    │   └── App.java
    └── test/java/com/example/rota
        ├── Checks.java
        └── Traps.java

In IntelliJ IDEA

Verified from the vendor docs, not executed: File → New → Project, pick Java, set the build system to IntelliJ, choose a JDK 21, and name it rota. Create the package com.example.rota under src, then mark the test folder as a test sources root (right-click it → Mark Directory as → Test Sources Root) if you keep the layout above.

In Eclipse

Verified from the vendor docs, not executed: File → New → Java Project, name it rota, and choose JavaSE-21 as the execution environment. Add the two source folders from the tree via Build Path → Configure Build Path → Source.

From the command line

Compile with javac -d out $(find src -name '*.java') and run a class with java -cp out com.example.rota.App. On Windows, use dir /s /b src\*.java > files.txt and javac -d out @files.txt. I didn't test Windows.

Write the rota, one class at a time

Step 1: Define the two enums

Goal: the constants everything else is keyed by.

src/main/java/com/example/rota/Permission.java

package com.example.rota;

public enum Permission {
    VIEW, EDIT, APPROVE, DEPLOY, ADMIN
}

src/main/java/com/example/rota/Severity.java

package com.example.rota;

public enum Severity {
    SEV1, SEV2, SEV3, SEV4
}

Order matters here. EnumSet and EnumMap both iterate in declaration order, so VIEW before ADMIN is a decision you're making for every printout. Move a constant later and every printed set changes with it.

Checkpoint: javac -d out src/main/java/com/example/rota/Permission.java src/main/java/com/example/rota/Severity.java prints nothing.

Step 2: Give each role an EnumSet of permissions

Goal: a role is a set of permissions, and "what can't this role do" is one call.

src/main/java/com/example/rota/Role.java

package com.example.rota;

import java.util.EnumSet;
import java.util.Set;

public enum Role {
    VIEWER(EnumSet.of(Permission.VIEW)),
    ENGINEER(EnumSet.of(Permission.VIEW, Permission.EDIT, Permission.DEPLOY)),
    LEAD(EnumSet.range(Permission.VIEW, Permission.DEPLOY)),
    ADMIN(EnumSet.allOf(Permission.class));

    private final Set<Permission> permissions;

    Role(EnumSet<Permission> permissions) {
        this.permissions = permissions;
    }

    /** A copy, so callers can't change the role's own set. */
    public Set<Permission> permissions() {
        return EnumSet.copyOf(permissions);
    }

    public boolean can(Permission p) {
        return permissions.contains(p);
    }

    /** Everything this role can't do. */
    public Set<Permission> missing() {
        return EnumSet.complementOf(EnumSet.copyOf(permissions));
    }
}

Three things to notice. EnumSet.of(...) lists them. EnumSet.range(VIEW, DEPLOY) takes everything between two constants in declaration order, which is why LEAD gets four permissions in one expression. EnumSet.allOf(Permission.class) is the "every constant" set. And complementOf gives you the opposite, so missing() needs no loop.

permissions() returns a copy. Without that, a caller could add ADMIN to the VIEWER role's own set. Check 17 later proves it.

Step 3: Write the rota with an EnumMap

Goal: one person per day, gaps in one call, and a per-engineer grouping.

src/main/java/com/example/rota/Incident.java

package com.example.rota;

import java.time.DayOfWeek;

public record Incident(int id, DayOfWeek day, Severity severity, String title) {
}

src/main/java/com/example/rota/Rota.java

package com.example.rota;

import java.time.DayOfWeek;
import java.util.Collection;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

public class Rota {

    public static final Set<DayOfWeek> WEEKDAYS =
            EnumSet.range(DayOfWeek.MONDAY, DayOfWeek.FRIDAY);
    public static final Set<DayOfWeek> WEEKEND =
            EnumSet.complementOf(EnumSet.copyOf(WEEKDAYS));

    private final Map<DayOfWeek, String> onCall = new EnumMap<>(DayOfWeek.class);

    public Rota assign(DayOfWeek day, String engineer) {
        onCall.put(day, engineer);
        return this;
    }

    public Rota assignAll(Set<DayOfWeek> days, String engineer) {
        days.forEach(d -> onCall.put(d, engineer));
        return this;
    }

    public Optional<String> whoIsOn(DayOfWeek day) {
        return Optional.ofNullable(onCall.get(day));
    }

    /** Days nobody covers. */
    public Set<DayOfWeek> gaps() {
        return EnumSet.complementOf(coveredDays());
    }

    private EnumSet<DayOfWeek> coveredDays() {
        // copyOf(Collection) throws on an empty plain collection, so branch for the empty rota
        return onCall.isEmpty()
                ? EnumSet.noneOf(DayOfWeek.class)
                : EnumSet.copyOf(onCall.keySet());
    }

    /** Which days each engineer covers, in week order. */
    public Map<String, Set<DayOfWeek>> daysByEngineer() {
        return onCall.entrySet().stream().collect(Collectors.groupingBy(
                Map.Entry::getValue,
                java.util.TreeMap::new,
                Collectors.mapping(Map.Entry::getKey,
                        Collectors.toCollection(() -> EnumSet.noneOf(DayOfWeek.class)))));
    }

    /** Incident counts per severity, with a zero for severities that had none. */
    public static Map<Severity, Long> countBySeverity(Collection<Incident> incidents) {
        Map<Severity, Long> counts = incidents.stream().collect(Collectors.groupingBy(
                Incident::severity,
                () -> new EnumMap<>(Severity.class),
                Collectors.counting()));
        for (Severity s : Severity.values()) {
            counts.putIfAbsent(s, 0L);
        }
        return counts;
    }

    public Map<DayOfWeek, String> snapshot() {
        return new EnumMap<>(onCall);
    }
}

Why it's shaped like this:

  • new EnumMap<>(DayOfWeek.class) needs the class literal, because the map allocates one slot per constant up front and has to know how many there are.
  • WEEKEND is the complement of WEEKDAYS. Define the second from the first and they can't drift.
  • coveredDays() has a branch for the empty rota. This is where I learned about EnumSet.copyOf(Collection): handed an empty plain collection, it can't tell which enum type you mean, so it throws. Trap 4 shows the message.
  • daysByEngineer() uses a TreeMap for names and a noneOf-based EnumSet for days, so both come out sorted.
  • countBySeverity passes () -> new EnumMap<>(Severity.class) to groupingBy. Leave that argument out and you get a HashMap back (trap 8). The loop after it adds zero counts, because a map built from a stream only has keys that occurred.

Checkpoint: javac -d out $(find src/main -name '*.java') prints nothing. I compiled with -Xlint:all and got no warnings.

Step 4: Print it

Goal: one main that exercises everything.

src/main/java/com/example/rota/App.java

package com.example.rota;

import java.time.DayOfWeek;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;

public class App {
    public static void main(String[] args) {
        Rota rota = new Rota()
                .assign(DayOfWeek.MONDAY, "Priya")
                .assign(DayOfWeek.TUESDAY, "Priya")
                .assign(DayOfWeek.WEDNESDAY, "Marcus")
                .assign(DayOfWeek.FRIDAY, "Marcus")
                .assign(DayOfWeek.SUNDAY, "Aiko");

        System.out.println("Who is on call:");
        for (DayOfWeek d : DayOfWeek.values()) {
            System.out.printf("  %-9s %s%n", d, rota.whoIsOn(d).orElse("-- nobody --"));
        }
        System.out.println("Gaps: " + rota.gaps());
        System.out.println("Weekday gaps: " + gapsIn(rota, Rota.WEEKDAYS));
        System.out.println("By engineer: " + rota.daysByEngineer());

        System.out.println();
        for (Role r : Role.values()) {
            System.out.printf("%-8s can %s, cannot %s%n", r, r.permissions(), r.missing());
        }

        List<Incident> incidents = List.of(
                new Incident(1, DayOfWeek.MONDAY, Severity.SEV3, "Slow search"),
                new Incident(2, DayOfWeek.WEDNESDAY, Severity.SEV1, "Payments down"),
                new Incident(3, DayOfWeek.WEDNESDAY, Severity.SEV3, "Email delay"),
                new Incident(4, DayOfWeek.SATURDAY, Severity.SEV3, "Cert expiring"));
        System.out.println();
        System.out.println("Incidents: " + Rota.countBySeverity(incidents));

        Map<DayOfWeek, Integer> perDay = new EnumMap<>(DayOfWeek.class);
        for (Incident i : incidents) {
            perDay.merge(i.day(), 1, Integer::sum);
        }
        System.out.println("Per day: " + perDay);
    }

    static java.util.Set<DayOfWeek> gapsIn(Rota rota, java.util.Set<DayOfWeek> window) {
        java.util.Set<DayOfWeek> g = java.util.EnumSet.copyOf(rota.gaps());
        g.retainAll(window);
        return g;
    }
}

Read the output line by line

javac -d out $(find src/main -name '*.java')
java -cp out com.example.rota.App
Who is on call:
  MONDAY    Priya
  TUESDAY   Priya
  WEDNESDAY Marcus
  THURSDAY  -- nobody --
  FRIDAY    Marcus
  SATURDAY  -- nobody --
  SUNDAY    Aiko
Gaps: [THURSDAY, SATURDAY]
Weekday gaps: [THURSDAY]
By engineer: {Aiko=[SUNDAY], Marcus=[WEDNESDAY, FRIDAY], Priya=[MONDAY, TUESDAY]}

VIEWER   can [VIEW], cannot [EDIT, APPROVE, DEPLOY, ADMIN]
ENGINEER can [VIEW, EDIT, DEPLOY], cannot [APPROVE, ADMIN]
LEAD     can [VIEW, EDIT, APPROVE, DEPLOY], cannot [ADMIN]
ADMIN    can [VIEW, EDIT, APPROVE, DEPLOY, ADMIN], cannot []

Incidents: {SEV1=1, SEV2=0, SEV3=3, SEV4=0}
Per day: {MONDAY=1, WEDNESDAY=2, SATURDAY=1}

Thursday and Saturday show as gaps, and Weekday gaps narrows that to Thursday because retainAll intersects the gap set with WEEKDAYS. The Per day line has no THURSDAY=0 entry. An EnumMap only contains keys you've put in; it doesn't pre-fill every constant. That's why countBySeverity fills zeros by hand.

I also compiled the main sources with javac --release 17 and it was clean, so nothing here needs Java 21 features.

Check it with 22 assertions

Maven Central wasn't reachable from my build environment, so I couldn't pull JUnit. The checks are a plain main with a check(name, boolean) helper. When you have JUnit, each check call becomes an @Test with an AssertJ assertion; my JUnit 5 article covers that setup.

src/test/java/com/example/rota/Checks.java

package com.example.rota;

import java.time.DayOfWeek;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class Checks {
    static int passed, failed;

    static void check(String name, boolean ok) {
        if (ok) { passed++; System.out.println("PASS  " + name); }
        else { failed++; System.out.println("FAIL  " + name); }
    }

    public static void main(String[] args) {
        Rota rota = new Rota().assign(DayOfWeek.MONDAY, "Priya").assign(DayOfWeek.FRIDAY, "Marcus");
        check("whoIsOn finds assigned day", rota.whoIsOn(DayOfWeek.MONDAY).orElse("").equals("Priya"));
        check("whoIsOn empty for gap", rota.whoIsOn(DayOfWeek.TUESDAY).isEmpty());
        check("gaps has 5 days", rota.gaps().size() == 5);
        check("gaps excludes covered day", !rota.gaps().contains(DayOfWeek.MONDAY));
        check("empty rota: all 7 days are gaps", new Rota().gaps().size() == 7);
        check("full rota: no gaps", new Rota().assignAll(EnumSet.allOf(DayOfWeek.class), "A").gaps().isEmpty());
        check("WEEKDAYS is Mon-Fri", Rota.WEEKDAYS.size() == 5 && Rota.WEEKDAYS.contains(DayOfWeek.FRIDAY));
        check("WEEKEND is Sat+Sun", Rota.WEEKEND.equals(EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY)));
        check("assignAll covers weekdays", new Rota().assignAll(Rota.WEEKDAYS, "A").gaps().equals(Rota.WEEKEND));

        List<DayOfWeek> order = new ArrayList<>(EnumSet.of(DayOfWeek.SUNDAY, DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY));
        check("EnumSet iterates in declaration order", order.equals(List.of(DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.SUNDAY)));
        check("daysByEngineer groups in week order",
                new Rota().assign(DayOfWeek.FRIDAY, "A").assign(DayOfWeek.MONDAY, "A")
                        .daysByEngineer().get("A").toString().equals("[MONDAY, FRIDAY]"));

        check("VIEWER cannot EDIT", !Role.VIEWER.can(Permission.EDIT));
        check("LEAD can APPROVE", Role.LEAD.can(Permission.APPROVE));
        check("LEAD cannot ADMIN", !Role.LEAD.can(Permission.ADMIN));
        check("ADMIN has every permission", Role.ADMIN.permissions().size() == Permission.values().length);
        check("ADMIN missing nothing", Role.ADMIN.missing().isEmpty());
        Set<Permission> copy = Role.VIEWER.permissions();
        copy.add(Permission.ADMIN);
        check("permissions() returns a defensive copy", !Role.VIEWER.can(Permission.ADMIN));

        List<Incident> inc = List.of(
                new Incident(1, DayOfWeek.MONDAY, Severity.SEV3, "a"),
                new Incident(2, DayOfWeek.MONDAY, Severity.SEV1, "b"));
        Map<Severity, Long> counts = Rota.countBySeverity(inc);
        check("countBySeverity result is an EnumMap", counts instanceof EnumMap);
        check("countBySeverity fills zeros", counts.get(Severity.SEV4) == 0L && counts.size() == 4);
        check("countBySeverity keys in severity order", new ArrayList<>(counts.keySet()).equals(List.of(Severity.values())));
        check("countBySeverity of nothing is four zeros", Rota.countBySeverity(List.of()).values().stream().allMatch(v -> v == 0));
        check("snapshot is independent", snapshotIndependent());

        System.out.println(passed + " passed, " + failed + " failed");
        if (failed > 0) System.exit(1);
    }

    static boolean snapshotIndependent() {
        Rota r = new Rota().assign(DayOfWeek.MONDAY, "A");
        Map<DayOfWeek, String> snap = r.snapshot();
        r.assign(DayOfWeek.TUESDAY, "B");
        return snap.size() == 1;
    }
}
javac -d out $(find src -name '*.java')
java -cp out com.example.rota.Checks
PASS  whoIsOn finds assigned day
PASS  whoIsOn empty for gap
PASS  gaps has 5 days
PASS  gaps excludes covered day
PASS  empty rota: all 7 days are gaps
PASS  full rota: no gaps
PASS  WEEKDAYS is Mon-Fri
PASS  WEEKEND is Sat+Sun
PASS  assignAll covers weekdays
PASS  EnumSet iterates in declaration order
PASS  daysByEngineer groups in week order
PASS  VIEWER cannot EDIT
PASS  LEAD can APPROVE
PASS  LEAD cannot ADMIN
PASS  ADMIN has every permission
PASS  ADMIN missing nothing
PASS  permissions() returns a defensive copy
PASS  countBySeverity result is an EnumMap
PASS  countBySeverity fills zeros
PASS  countBySeverity keys in severity order
PASS  countBySeverity of nothing is four zeros
PASS  snapshot is independent
22 passed, 0 failed

To make sure the checks can fail, I changed coveredDays() to always call EnumSet.copyOf(onCall.keySet()) (dropping the empty branch). The run died on the empty-rota check with IllegalArgumentException: Collection is empty at Rota.coveredDays. I put the code back after.

What the JDK stores underneath

EnumSet is abstract. EnumSet.of(...) and friends hand you a RegularEnumSet when the enum has 64 constants or fewer, and a JumboEnumSet above that. The regular one keeps membership in a single long: one bit per constant, set at position ordinal(). contains, add and remove are bit operations, and complementOf is a bitwise NOT masked to the number of constants. That's why missing() costs almost nothing.

EnumMap is an Object[] indexed by ordinal(), plus a cached array of the enum's constants. Lookup is an array read. No hashing, no collisions, no resizing, and iteration walks the array in order. Nulls are rejected as keys but allowed as values, per the API docs.

Both iterate in declaration order, both are not thread-safe (wrap with Collections.synchronizedMap or synchronizedSet if you must share them), and both have weakly consistent iterators. That last point matters less here than for a ConcurrentHashMap; see How to use ConcurrentHashMap in Java for how weak consistency feels in practice. For a hash-versus-array performance comparison with real numbers, the performance series has java-enummap-enumset (a companion piece on the same collections, written for speed instead of usage).

Ten behaviours worth seeing for yourself

Here's the trap file.

src/test/java/com/example/rota/Traps.java

package com.example.rota;

import java.time.DayOfWeek;
import java.util.*;
import java.util.stream.*;

public class Traps {
    interface Thrower { void run() throws Exception; }

    static void trap(String label, Thrower t) {
        try { t.run(); }
        catch (Exception e) { System.out.println(label + " -> " + e.getClass().getSimpleName() + ": " + e.getMessage()); }
    }

    public static void main(String[] args) {
        Map<DayOfWeek, String> m = new EnumMap<>(DayOfWeek.class);
        trap("1 put(null, ..)", () -> m.put(null, "x"));
        System.out.println("2 get(null) -> " + m.get(null) + ", containsKey(null) -> " + m.containsKey(null));
        trap("3 EnumSet.add(null)", () -> EnumSet.noneOf(DayOfWeek.class).add(null));

        List<DayOfWeek> empty = new ArrayList<>();
        trap("4 EnumSet.copyOf(empty list)", () -> EnumSet.copyOf(empty));
        System.out.println("5 copyOf(empty EnumSet) -> " + EnumSet.copyOf(EnumSet.noneOf(DayOfWeek.class)));

        Map<DayOfWeek, String> plainEmpty = new HashMap<>();
        trap("6 new EnumMap(empty HashMap)", () -> new EnumMap<>(plainEmpty));

        Map<DayOfWeek, String> hash = new HashMap<>();
        Map<DayOfWeek, String> tree = new EnumMap<>(DayOfWeek.class);
        for (DayOfWeek d : List.of(DayOfWeek.SUNDAY, DayOfWeek.MONDAY, DayOfWeek.THURSDAY, DayOfWeek.TUESDAY, DayOfWeek.SATURDAY)) {
            hash.put(d, "x"); tree.put(d, "x");
        }
        System.out.println("7 HashMap order -> " + hash.keySet());
        System.out.println("7 EnumMap order -> " + tree.keySet());

        List<Incident> inc = List.of(new Incident(1, DayOfWeek.MONDAY, Severity.SEV3, "a"));
        Map<Severity, Long> plain = inc.stream().collect(Collectors.groupingBy(Incident::severity, Collectors.counting()));
        System.out.println("8 default groupingBy -> " + plain.getClass().getSimpleName() + " " + plain);

        EnumMap<Severity, Integer> sizes = new EnumMap<>(Severity.class);
        System.out.println("9 fresh EnumMap size -> " + sizes.size() + ", get(SEV1) -> " + sizes.get(Severity.SEV1));
        trap("10 sizes.get(SEV1) + 1 unboxed", () -> { int x = sizes.get(Severity.SEV1) + 1; });
    }
}
java -cp out com.example.rota.Traps
1 put(null, ..) -> NullPointerException: Cannot invoke "Object.getClass()" because "key" is null
2 get(null) -> null, containsKey(null) -> false
3 EnumSet.add(null) -> NullPointerException: Cannot invoke "Object.getClass()" because "e" is null
4 EnumSet.copyOf(empty list) -> IllegalArgumentException: Collection is empty
5 copyOf(empty EnumSet) -> []
6 new EnumMap(empty HashMap) -> IllegalArgumentException: Specified map is empty
7 HashMap order -> [MONDAY, SUNDAY, THURSDAY, TUESDAY, SATURDAY]
7 EnumMap order -> [MONDAY, TUESDAY, THURSDAY, SATURDAY, SUNDAY]
8 default groupingBy -> HashMap {SEV3=1}
9 fresh EnumMap size -> 0, get(SEV1) -> null
10 sizes.get(SEV1) + 1 unboxed -> NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.EnumMap.get(Object)" is null

What each one means:

  1. EnumMap.put(null, ...) throws NullPointerException. The message in your JDK may differ; mine is a helpful NPE about key.
  2. get(null) and containsKey(null) don't throw. They return null and false. So reads are forgiving and writes aren't.
  3. EnumSet.add(null) also throws. Same rule.
  4. EnumSet.copyOf(emptyList) throws IllegalArgumentException: Collection is empty. The set can't discover the enum type from zero elements. Use EnumSet.noneOf(DayOfWeek.class) and addAll, which is why coveredDays() has its branch.
  5. EnumSet.copyOf(emptyEnumSet) is fine, because an EnumSet argument carries its type. The failure is only for plain collections.
  6. new EnumMap<>(emptyHashMap) throws Specified map is empty for the same reason. Copying from another EnumMap works even when it's empty, which is why snapshot() can stay a one-liner. My first draft had a needless ternary there.
  7. Iteration order. The HashMap order depends on identity hashes of the enum constants, so it changes from run to run. The EnumMap order is Monday through Sunday every time.
  8. groupingBy without a map factory gives you a HashMap. You lose the order and the compact storage. Pass () -> new EnumMap<>(Severity.class).
  9. A fresh EnumMap is empty, not full of zeros. size() is 0 and get returns null.
  10. Unboxing that null throws. sizes.get(SEV1) + 1 is an NPE. Use getOrDefault(SEV1, 0) or merge, as App does with perDay.merge.

One more I hit by accident: EnumMap<String, Integer> doesn't compile.

Bad.java:4: error: type argument String is not within bounds of type-variable K
        EnumMap<String, Integer> m = new EnumMap<>(String.class);
                ^
  where K is a type-variable:
    K extends Enum<K> declared in class EnumMap

That's the compiler telling you the key type must be an enum. It's the same message you'd get passing a plain class to any K extends Enum<K> API.

Where these two don't fit

  • Enums that change often. Because the order is the declaration order, adding a constant in the middle changes printed output and anything you persisted by ordinal. Persist by name().
  • Shared mutable state. Neither is thread-safe. For a concurrent version, wrap with the Collections.synchronized* methods or hold a lock, or use immutable snapshots as snapshot() does.
  • Immutable views. There's no unmodifiable EnumSet factory. Wrap with Collections.unmodifiableSet(...), or use Set.copyOf if you don't need declaration order (you'll lose it).
  • Sorted by something other than ordinal. Use a TreeMap with a comparator. My PriorityQueue and TreeMap article covers it, and LinkedHashMap is the choice when you need insertion order instead.

If you're modelling transitions between enum states, the next step is a state machine; that article is planned here.

The rules, short

  • Use EnumSet instead of HashSet<SomeEnum> and EnumMap instead of HashMap<SomeEnum, ...>.
  • Create with noneOf, allOf, of, range, or complementOf; use copyOf only when you know the source isn't an empty plain collection.
  • Pass an EnumMap factory to groupingBy/toMap, or the map isn't an EnumMap.
  • Fill defaults yourself, and use getOrDefault or merge rather than unboxing get.
  • Never pass null in; reads tolerate it, writes don't.
  • Return copies of internal EnumSets, because they're mutable.

EnumMap and EnumSet FAQ

When should I use EnumMap instead of HashMap?
Whenever the key type is an enum. You get ordinal-order iteration and array-backed storage with no hashing. Use HashMap only if keys may be null or the key type isn't an enum.

How do I get all values of an enum into an EnumSet?
EnumSet.allOf(Permission.class). For none, EnumSet.noneOf(Permission.class). For everything but a few, EnumSet.complementOf(EnumSet.of(...)).

Is EnumSet thread-safe?
No. Neither is EnumMap. Synchronise externally, or share immutable copies.

Why does EnumSet.copyOf throw "Collection is empty"?
copyOf(Collection) reads the enum type from the first element. With none, there's nothing to read. Use noneOf(type) plus addAll, or copy from another EnumSet.

References

java-collectionsjava-tutorialshow-to-tutorials

Staff engineer architecting Kafka and Flink platforms for banking, where a dropped event is a reconciliation break, not a retry. Through javahandbook.com, I share production-tested guidance on building reliable event-driven systems.

Comments