> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# How to Use EnumMap and EnumSet in Java 21 (Rota Example)
- URL: https://www.ggorantala.dev/how-to-use-enummap-enumset-java/
- Published: 2026-07-21T05:13:00.000Z
- Updated: 2026-10-31T06:32:46.000Z
- Description: 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.
- Author: Gopi Gorantala
- Tags: java-collections, java-tutorials, how-to-tutorials

## 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:

```log
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](https://ggorantala.dev/how-to-use-java-enums-fields-methods/?ref=ggorantala.dev) 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.

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

```

The tree you're building:

```log
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`

```java
package com.example.rota;

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

```

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

```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`

```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`

```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`

```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`

```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

```bash
javac -d out $(find src/main -name '*.java')
java -cp out com.example.rota.App

```

```log
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](https://ggorantala.dev/how-to-write-unit-tests-junit-5-java/?ref=ggorantala.dev) covers that setup.

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

```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;
    }
}

```

```bash
javac -d out $(find src -name '*.java')
java -cp out com.example.rota.Checks

```

```log
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](https://ggorantala.dev/how-to-use-concurrenthashmap-java/?ref=ggorantala.dev) for how weak consistency feels in practice. For a hash-versus-array performance comparison with real numbers, the performance series has [java-enummap-enumset](https://ggorantala.dev/java-enummap-enumset/?ref=ggorantala.dev) (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`

```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; });
    }
}

```

```bash
java -cp out com.example.rota.Traps

```

```log
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.

```log
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](https://ggorantala.dev/how-to-use-priorityqueue-treemap-java/?ref=ggorantala.dev) covers it, and [LinkedHashMap](https://ggorantala.dev/how-to-use-linkedhashmap-java/?ref=ggorantala.dev) 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](https://ggorantala.dev/how-to-implement-state-machine-pattern-java/?ref=ggorantala.dev).

## 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 `EnumSet`s, 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 SE 21 API: [EnumMap](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/EnumMap.html?ref=ggorantala.dev), [EnumSet](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/EnumSet.html?ref=ggorantala.dev), [Collectors.groupingBy](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/Collectors.html?ref=ggorantala.dev)
- Java SE 21 API: [DayOfWeek](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/DayOfWeek.html?ref=ggorantala.dev)
- Related on this site: [Introduction to Java enums](https://ggorantala.dev/introduction-to-java-enums/?ref=ggorantala.dev), [Collect stream results](https://ggorantala.dev/how-to-collect-stream-results-java/?ref=ggorantala.dev)