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.WEEKENDis the complement ofWEEKDAYS. Define the second from the first and they can't drift.coveredDays()has a branch for the empty rota. This is where I learned aboutEnumSet.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 aTreeMapfor names and anoneOf-basedEnumSetfor days, so both come out sorted.countBySeveritypasses() -> new EnumMap<>(Severity.class)togroupingBy. Leave that argument out and you get aHashMapback (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:
EnumMap.put(null, ...)throwsNullPointerException. The message in your JDK may differ; mine is a helpful NPE aboutkey.get(null)andcontainsKey(null)don't throw. They returnnullandfalse. So reads are forgiving and writes aren't.EnumSet.add(null)also throws. Same rule.EnumSet.copyOf(emptyList)throwsIllegalArgumentException: Collection is empty. The set can't discover the enum type from zero elements. UseEnumSet.noneOf(DayOfWeek.class)andaddAll, which is whycoveredDays()has its branch.EnumSet.copyOf(emptyEnumSet)is fine, because anEnumSetargument carries its type. The failure is only for plain collections.new EnumMap<>(emptyHashMap)throwsSpecified map is emptyfor the same reason. Copying from anotherEnumMapworks even when it's empty, which is whysnapshot()can stay a one-liner. My first draft had a needless ternary there.- Iteration order. The
HashMaporder depends on identity hashes of the enum constants, so it changes from run to run. TheEnumMaporder is Monday through Sunday every time. groupingBywithout a map factory gives you aHashMap. You lose the order and the compact storage. Pass() -> new EnumMap<>(Severity.class).- A fresh
EnumMapis empty, not full of zeros.size()is 0 andgetreturnsnull. - Unboxing that
nullthrows.sizes.get(SEV1) + 1is an NPE. UsegetOrDefault(SEV1, 0)ormerge, asAppdoes withperDay.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 assnapshot()does. - Immutable views. There's no unmodifiable
EnumSetfactory. Wrap withCollections.unmodifiableSet(...), or useSet.copyOfif you don't need declaration order (you'll lose it). - Sorted by something other than ordinal. Use a
TreeMapwith 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
EnumSetinstead ofHashSet<SomeEnum>andEnumMapinstead ofHashMap<SomeEnum, ...>. - Create with
noneOf,allOf,of,range, orcomplementOf; usecopyOfonly when you know the source isn't an empty plain collection. - Pass an
EnumMapfactory togroupingBy/toMap, or the map isn't anEnumMap. - Fill defaults yourself, and use
getOrDefaultormergerather than unboxingget. - Never pass
nullin; 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 SE 21 API: EnumMap, EnumSet, Collectors.groupingBy
- Java SE 21 API: DayOfWeek
- Related on this site: Introduction to Java enums, Collect stream results