EnumMap and EnumSet in Java: The Fast, Type-Safe Collections You're Probably Not Using
Most Java developers reach for HashMap and HashSet the moment they need a collection, without stopping to ask whether the key or element type is an enum. That habit is costing them performance and clarity. EnumMap and EnumSet are purpose-built for enums, and they're backed by arrays and bit vectors respectively β data structures that are orders of magnitude faster than the hash-based alternatives at the scales you'll encounter in real applications.
The problem HashMap has with enum keys isn't correctness β it's waste. HashMap has to compute a hash code, handle potential collisions, and allocate Entry objects on the heap for every key-value pair. But enums have a fixed, known ordinal at compile time. That means you can use the ordinal as a direct array index, skipping all the hashing overhead entirely. EnumSet goes even further: if your enum has 64 or fewer constants (which covers almost every real-world enum you'll ever write), it stores the entire set as a single long using bitwise operations. A membership check becomes a single bitwise AND β it doesn't get faster than that on the JVM.
By the end of this article you'll understand exactly how EnumMap and EnumSet work under the hood, when to reach for them instead of their general-purpose counterparts, and how to use them in practical patterns like state machines, feature flag systems, and role-based access control. You'll also know the two mistakes that trip up nearly every developer the first time they use these collections.
How EnumMap Works β and Why It Beats HashMap for Enum Keys
EnumMap stores its values in a plain Object array, sized exactly to the number of constants in your enum. The key's ordinal() β a zero-based integer assigned to each enum constant at compile time β is used as the array index. No hashing, no collision resolution, no Entry wrappers. A put() is literally values[key.ordinal()] = value. A get() is values[key.ordinal()].
This has three practical consequences. First, it's faster than HashMap for both reads and writes because array index access is O(1) with zero overhead. Second, iteration always follows the declaration order of your enum constants, which makes output predictable and logs easier to read. Third, it uses less memory because there are no Entry objects, no load factor headroom, and no linked lists for collision chains.
The API is identical to Map, so switching from a HashMap
A classic real-world use case: mapping HTTP status categories, order statuses, or day-of-week schedules to some processing logic. Any time your keys are a closed, finite set modelled as an enum, EnumMap is the right choice.
import java.util.EnumMap; import java.util.Map; public class CafeOrderTracker { // A fixed set of drink sizes β perfect enum candidates enum DrinkSize { SMALL, MEDIUM, LARGE, EXTRA_LARGE } public static void main(String[] args) { // EnumMap constructor requires the enum's Class so it can // allocate a backing array of exactly the right size (4 slots here) EnumMap<DrinkSize, Integer> orderCounts = new EnumMap<>(DrinkSize.class); // Populating like any normal Map β the difference is in the internals orderCounts.put(DrinkSize.SMALL, 42); orderCounts.put(DrinkSize.MEDIUM, 78); orderCounts.put(DrinkSize.LARGE, 55); orderCounts.put(DrinkSize.EXTRA_LARGE, 19); System.out.println("=== Today's Order Summary ==="); // Iteration is GUARANTEED to follow enum declaration order (SMALL first) // This is NOT guaranteed with HashMap β huge win for readability for (Map.Entry<DrinkSize, Integer> entry : orderCounts.entrySet()) { System.out.printf("%-15s : %d orders%n", entry.getKey(), entry.getValue()); } // Updating a count β internally this is just array[2] = array[2] + 1 orderCounts.merge(DrinkSize.LARGE, 1, Integer::sum); System.out.println("\nAfter one more LARGE order: " + orderCounts.get(DrinkSize.LARGE)); // Check which sizes are currently tracked System.out.println("\nTracked sizes: " + orderCounts.keySet()); // computeIfAbsent works exactly as on HashMap orderCounts.computeIfAbsent(DrinkSize.SMALL, k -> 0); System.out.println("SMALL (already present, unchanged): " + orderCounts.get(DrinkSize.SMALL)); } }
SMALL : 42 orders
MEDIUM : 78 orders
LARGE : 55 orders
EXTRA_LARGE : 19 orders
After one more LARGE order: 56
Tracked sizes: [SMALL, MEDIUM, LARGE, EXTRA_LARGE]
SMALL (already present, unchanged): 42
How EnumSet Works β Bit Vectors and Why They're Blazing Fast
EnumSet is even more specialized than EnumMap. It doesn't use an array at all β it uses a bit vector. Each bit in a long primitive corresponds to one enum constant (via its ordinal). If the bit is 1, that constant is in the set. If it's 0, it's not. That's the entire data structure for enums with 64 or fewer constants (RegularEnumSet). For larger enums, JumpingEnumSet uses an array of longs β but you'll rarely hit that case.
What does this mean in practice? A contains() check is a single bitwise AND operation on a long. An addAll() of another EnumSet is a single bitwise OR. A removeAll() is a bitwise AND NOT. These operations run in constant time with almost zero CPU overhead and zero garbage β no Iterator objects, no boxing of primitives, nothing.
You never instantiate EnumSet with new. Instead, it gives you a rich set of static factory methods: of(), allOf(), noneOf(), range(), and copyOf(). This is a deliberate design choice β the factory can pick the right internal implementation (RegularEnumSet vs JumpingEnumSet) based on the enum size without exposing that detail to you.
Perfect use cases include permission systems, feature flags, day-of-week schedules, and any scenario where you need a subset of a fixed set of options.
import java.util.EnumSet; import java.util.Set; public class RolePermissionSystem { // Permissions modelled as an enum β the ideal candidate for EnumSet enum Permission { READ, WRITE, DELETE, PUBLISH, ADMIN } // Role definitions using EnumSet.of() β clear, readable, efficient static final Set<Permission> VIEWER_PERMISSIONS = EnumSet.of(Permission.READ); static final Set<Permission> EDITOR_PERMISSIONS = EnumSet.of(Permission.READ, Permission.WRITE); static final Set<Permission> PUBLISHER_PERMISSIONS = EnumSet.of(Permission.READ, Permission.WRITE, Permission.PUBLISH); // Admin gets everything β allOf() creates a full set in one shot static final Set<Permission> ADMIN_PERMISSIONS = EnumSet.allOf(Permission.class); public static boolean canPerform(Set<Permission> userPermissions, Permission requiredPermission) { // Under the hood for two EnumSets this is a single bitwise AND β extremely fast return userPermissions.contains(requiredPermission); } public static Set<Permission> combinePermissions(Set<Permission> roleA, Set<Permission> roleB) { // EnumSet.copyOf + addAll is one bitwise OR internally Set<Permission> combined = EnumSet.copyOf(roleA); combined.addAll(roleB); // bitwise OR on the backing long return combined; } public static void main(String[] args) { System.out.println("=== Permission Check ==="); System.out.println("Viewer can READ? " + canPerform(VIEWER_PERMISSIONS, Permission.READ)); System.out.println("Viewer can DELETE? " + canPerform(VIEWER_PERMISSIONS, Permission.DELETE)); System.out.println("Editor can WRITE? " + canPerform(EDITOR_PERMISSIONS, Permission.WRITE)); System.out.println("Editor can PUBLISH? " + canPerform(EDITOR_PERMISSIONS, Permission.PUBLISH)); System.out.println("\n=== Admin Permissions ==="); System.out.println("Admin has: " + ADMIN_PERMISSIONS); // noneOf creates an empty set β useful as a starting point to build up Set<Permission> temporaryAccess = EnumSet.noneOf(Permission.class); temporaryAccess.add(Permission.READ); temporaryAccess.add(Permission.WRITE); System.out.println("\nTemporary access: " + temporaryAccess); // range() selects a contiguous slice of enum constants by declaration order Set<Permission> readToPublish = EnumSet.range(Permission.READ, Permission.PUBLISH); System.out.println("READ through PUBLISH: " + readToPublish); // Combine editor + publisher roles Set<Permission> combinedRole = combinePermissions(EDITOR_PERMISSIONS, PUBLISHER_PERMISSIONS); System.out.println("\nCombined editor+publisher: " + combinedRole); // complementOf gives everything NOT in the set Set<Permission> nonAdminPerms = EnumSet.complementOf((EnumSet<Permission>) ADMIN_PERMISSIONS); System.out.println("Complement of ADMIN (empty): " + nonAdminPerms); } }
Viewer can READ? true
Viewer can DELETE? false
Editor can WRITE? true
Editor can PUBLISH? false
=== Admin Permissions ===
Admin has: [READ, WRITE, DELETE, PUBLISH, ADMIN]
Temporary access: [READ, WRITE]
READ through PUBLISH: [READ, WRITE, DELETE, PUBLISH]
Combined editor+publisher: [READ, WRITE, PUBLISH]
Complement of ADMIN (empty): []
Real-World Pattern: State Machine Using EnumMap and EnumSet Together
The real power of these two classes emerges when you combine them. A state machine is a textbook example: you have a fixed set of states (enum), and from each state, a fixed set of valid next states (EnumSet). The transition table is naturally an EnumMap where the key is the current state and the value is an EnumSet of valid transitions.
This pattern is used everywhere β order processing pipelines, connection lifecycle management, document approval workflows, UI navigation stacks. Modelling it with EnumMap + EnumSet gives you compile-time safety (you can't accidentally reference a state that doesn't exist), fast lookup, and self-documenting code where the transition table is readable at a glance.
The alternative β a HashMap
This pattern also happens to be what many workflow engines and state machine libraries use internally under the hood.
import java.util.EnumMap; import java.util.EnumSet; import java.util.Map; import java.util.Set; public class OrderStateMachine { enum OrderStatus { PENDING, CONFIRMED, PROCESSING, SHIPPED, DELIVERED, CANCELLED } // The transition table: for each status, which statuses can it move to? // EnumMap as the outer container, EnumSet as the inner β best of both worlds private static final Map<OrderStatus, Set<OrderStatus>> VALID_TRANSITIONS; static { VALID_TRANSITIONS = new EnumMap<>(OrderStatus.class); // Each entry defines legal forward transitions from that state VALID_TRANSITIONS.put(OrderStatus.PENDING, EnumSet.of(OrderStatus.CONFIRMED, OrderStatus.CANCELLED)); VALID_TRANSITIONS.put(OrderStatus.CONFIRMED, EnumSet.of(OrderStatus.PROCESSING, OrderStatus.CANCELLED)); VALID_TRANSITIONS.put(OrderStatus.PROCESSING, EnumSet.of(OrderStatus.SHIPPED, OrderStatus.CANCELLED)); VALID_TRANSITIONS.put(OrderStatus.SHIPPED, EnumSet.of(OrderStatus.DELIVERED)); // Can't cancel once shipped // Terminal states β no outgoing transitions VALID_TRANSITIONS.put(OrderStatus.DELIVERED, EnumSet.noneOf(OrderStatus.class)); VALID_TRANSITIONS.put(OrderStatus.CANCELLED, EnumSet.noneOf(OrderStatus.class)); } private OrderStatus currentStatus; public OrderStateMachine(OrderStatus initialStatus) { this.currentStatus = initialStatus; } public boolean transitionTo(OrderStatus nextStatus) { // contains() on an EnumSet is a bitwise AND β as fast as it gets Set<OrderStatus> allowedNext = VALID_TRANSITIONS.get(currentStatus); if (allowedNext.contains(nextStatus)) { System.out.printf("Transition: %s β %s β%n", currentStatus, nextStatus); currentStatus = nextStatus; return true; } else { System.out.printf("REJECTED: %s β %s is not a valid transition%n", currentStatus, nextStatus); return false; } } public OrderStatus getStatus() { return currentStatus; } public static void main(String[] args) { OrderStateMachine order = new OrderStateMachine(OrderStatus.PENDING); System.out.println("Initial status: " + order.getStatus()); System.out.println(); // Happy path order.transitionTo(OrderStatus.CONFIRMED); order.transitionTo(OrderStatus.PROCESSING); order.transitionTo(OrderStatus.SHIPPED); // Illegal jump β can't go from SHIPPED back to PENDING order.transitionTo(OrderStatus.PENDING); // Can't cancel after shipping either order.transitionTo(OrderStatus.CANCELLED); // Legal final step order.transitionTo(OrderStatus.DELIVERED); System.out.println("\nFinal status: " + order.getStatus()); // Demonstrate terminal state β no further transitions possible order.transitionTo(OrderStatus.CANCELLED); } }
Transition: PENDING β CONFIRMED β
Transition: CONFIRMED β PROCESSING β
Transition: PROCESSING β SHIPPED β
REJECTED: SHIPPED β PENDING is not a valid transition
REJECTED: SHIPPED β CANCELLED is not a valid transition
Transition: SHIPPED β DELIVERED β
Final status: DELIVERED
REJECTED: DELIVERED β CANCELLED is not a valid transition
| Feature / Aspect | EnumMap vs HashMap | EnumSet vs HashSet |
|---|---|---|
| Key/Element constraint | Keys must be a single enum type | Elements must be a single enum type |
| Internal data structure | Object array indexed by ordinal | Single long (bit vector) for β€64 constants |
| Time complexity (get/contains) | O(1) β direct array index, zero hashing | O(1) β single bitwise AND operation |
| Memory overhead | Low β no Entry objects, no load factor waste | Extremely low β entire set fits in one long |
| Null keys / elements | Does NOT allow null keys (throws NPE) | Does NOT allow null elements (throws NPE) |
| Iteration order | Always enum declaration order β guaranteed | Always enum declaration order β guaranteed |
| Thread safety | Not thread-safe β same as HashMap | Not thread-safe β same as HashSet |
| When to prefer | Enum keys, need keyβvalue mapping | Subset of enum constants, no values needed |
| Construction | new EnumMap<>(MyEnum.class) | EnumSet.of(), allOf(), noneOf(), range() |
| Serialisation | Serializable β safe to use in DTOs | Serializable β safe to use in DTOs |
π― Key Takeaways
- EnumMap replaces HashMap
with an array-backed structure that uses the enum's ordinal as a direct index β no hashing, no collisions, guaranteed declaration-order iteration. - EnumSet stores an entire set of enum constants in a single long primitive using bit manipulation β contains() is a bitwise AND, making it the fastest possible Set implementation for enum types.
- Both EnumMap and EnumSet forbid null keys/elements unlike their general-purpose counterparts β account for this when migrating existing code from HashMap or HashSet.
- The EnumMap + EnumSet combination is the natural fit for state machines, permission systems, and feature flags β it gives you compile-time type safety, zero-overhead lookups, and code that reads like a business specification.
β Common Mistakes to Avoid
- βMistake 1: Using null as a key in EnumMap or an element in EnumSet β Both collections explicitly forbid null. EnumMap.put(null, value) throws a NullPointerException immediately (unlike HashMap which allows one null key). Fix: always validate inputs before insertion, or use Optional to represent absent values in your domain model instead of relying on null keys.
- βMistake 2: Casting the result of EnumSet static factories to EnumSet when passing to complementOf() β complementOf() requires an EnumSet
parameter, not a Set . If you declared your variable as Set , the cast is unavoidable but can cause ClassCastException if the underlying type is ever changed. Fix: declare role constants as EnumSet (not Set ) when you know you'll need complementOf() or other EnumSet-specific methods. - βMistake 3: Forgetting that EnumMap iteration order reflects enum DECLARATION order, not insertion order β Developers migrating from LinkedHashMap sometimes rely on EnumMap to preserve insertion order, but it doesn't. EnumMap always iterates in the order constants are declared in the enum source code. Fix: if you need a different display order, re-order your enum constants deliberately, or sort entries explicitly before display β don't assume insertion order is preserved.
Interview Questions on This Topic
- QWhy is EnumMap faster than HashMap when the key type is an enum? Walk me through what happens internally when you call get() on each one.
- QEnumSet doesn't have a public constructor β you can only create instances via static factory methods like EnumSet.of() or EnumSet.allOf(). Why was it designed that way, and what design pattern does this represent?
- QIf you have an EnumSet declared as Set
and you want to call EnumSet.complementOf() on it, what problem do you run into and how would you solve it without breaking encapsulation?
Frequently Asked Questions
Can I use EnumMap or EnumSet with a null key or null element?
No β both collections throw a NullPointerException if you attempt to insert null. This is intentional: since every enum constant has a guaranteed non-null ordinal, null has no meaningful representation in either collection. If you need to represent 'no value', use Optional in your value type or handle the null case before interacting with the collection.
Is EnumSet thread-safe?
No, EnumSet is not thread-safe, just like HashSet. If you need a thread-safe version, wrap it with Collections.synchronizedSet(EnumSet.of(...)), or use a CopyOnWriteArraySet if reads vastly outnumber writes. For high-contention scenarios, consider an AtomicLong and manage the bit manipulations manually β though that's rarely necessary.
What's the difference between EnumSet.of() and EnumSet.allOf()?
EnumSet.of() creates a set containing exactly the constants you specify as arguments β great for defining specific subsets like a role's permissions. EnumSet.allOf(MyEnum.class) creates a set containing every single constant in the enum β useful as a starting point when you want to remove exceptions rather than add inclusions, or when you need to iterate all constants without hardcoding them.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful β not just SEO filler.