Homeβ€Ί Javaβ€Ί EnumMap and EnumSet in Java: The Fast, Type-Safe Collections You're Probably Not Using

EnumMap and EnumSet in Java: The Fast, Type-Safe Collections You're Probably Not Using

In Plain English πŸ”₯
Imagine you run a small cafΓ© with exactly four drink sizes: Small, Medium, Large, and XL. Every morning you count how many of each size you sold. You could use a giant notebook with thousands of blank pages (a regular HashMap), or you could use a pre-printed form with exactly four rows β€” one for each size. That pre-printed form is an EnumMap. It's leaner, faster, and impossible to accidentally write 'Gigantic' in a row that doesn't exist. EnumSet is the same idea, but instead of storing counts, it's a checklist β€” tick which sizes are available today.
⚑ Quick Answer
Imagine you run a small cafΓ© with exactly four drink sizes: Small, Medium, Large, and XL. Every morning you count how many of each size you sold. You could use a giant notebook with thousands of blank pages (a regular HashMap), or you could use a pre-printed form with exactly four rows β€” one for each size. That pre-printed form is an EnumMap. It's leaner, faster, and impossible to accidentally write 'Gigantic' in a row that doesn't exist. EnumSet is the same idea, but instead of storing counts, it's a checklist β€” tick which sizes are available today.

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 to an EnumMap is usually a one-line change. The constructor just needs the enum's Class object so it knows how large to make the backing array.

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.

CafeOrderTracker.java Β· JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243
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));
    }
}
β–Ά Output
=== Today's Order Summary ===
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
⚠️
Pro Tip: Declare as Map, not EnumMapProgram to the interface: declare your variable as Map but instantiate with new EnumMap<>(DrinkSize.class). This keeps your code flexible β€” you can swap in a HashMap for testing or if the key type ever changes β€” while still getting all the EnumMap performance in production.

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.

RolePermissionSystem.java Β· JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
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);
    }
}
β–Ά Output
=== Permission Check ===
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): []
πŸ”₯
Interview Gold: Why no new EnumSet()?EnumSet's constructor is package-private by design. The static factories (of, allOf, noneOf, range) let the JDK choose between RegularEnumSet (single long, for ≀64 constants) and JumpingEnumSet (long array, for >64 constants) transparently. This is the Factory Method pattern in the standard library β€” interviewers love this answer.

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> with magic strings β€” is the kind of code that produces 2am production incidents. An EnumMap> makes illegal transitions visible at compile time and nearly impossible to introduce accidentally.

This pattern also happens to be what many workflow engines and state machine libraries use internally under the hood.

OrderStateMachine.java Β· JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
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);
    }
}
β–Ά Output
Initial status: PENDING

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
⚠️
Pro Tip: Make the transition table unmodifiableWrap VALID_TRANSITIONS with Collections.unmodifiableMap() and each inner EnumSet with Collections.unmodifiableSet() before storing them. This prevents any code from accidentally mutating the transition rules at runtime β€” a subtle bug that's incredibly hard to track down in production.
Feature / AspectEnumMap vs HashMapEnumSet vs HashSet
Key/Element constraintKeys must be a single enum typeElements must be a single enum type
Internal data structureObject array indexed by ordinalSingle long (bit vector) for ≀64 constants
Time complexity (get/contains)O(1) β€” direct array index, zero hashingO(1) β€” single bitwise AND operation
Memory overheadLow β€” no Entry objects, no load factor wasteExtremely low β€” entire set fits in one long
Null keys / elementsDoes NOT allow null keys (throws NPE)Does NOT allow null elements (throws NPE)
Iteration orderAlways enum declaration order β€” guaranteedAlways enum declaration order β€” guaranteed
Thread safetyNot thread-safe β€” same as HashMapNot thread-safe β€” same as HashSet
When to preferEnum keys, need key→value mappingSubset of enum constants, no values needed
Constructionnew EnumMap<>(MyEnum.class)EnumSet.of(), allOf(), noneOf(), range()
SerialisationSerializable β€” safe to use in DTOsSerializable β€” 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.

πŸ”₯
TheCodeForge Editorial Team Verified Author

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.

← PreviousLabeled break and continue in JavaNext β†’WeakHashMap in Java
Forged with πŸ”₯ at TheCodeForge.io β€” Where Developers Are Forged