Back to Special Maps & Collections
Q4Question 4 of 5

What are EnumSet and EnumMap?

Key Concepts
  • EnumSet stores enum constants as bits in a long (or long[]) where each bit position corresponds to one constant's ordinal.
  • EnumMap stores values in a plain Object array indexed by the enum constant's ordinal, giving O(1) direct array access with no hashing.
  • EnumSet operations (add, remove, contains) are single bitwise instructions and run in constant time.
  • EnumSet uses RegularEnumSet (one long, up to 64 constants) or JumboEnumSet (long[] for larger enums).
  • EnumMap iteration follows the enum declaration order, not insertion order.
  • Both are dramatically faster and more memory-efficient than HashSet and HashMap for enum-typed keys.
  • Always prefer EnumSet over HashSet and EnumMap over HashMap when the key type is an enum.

Definition

EnumSet is a Set backed by a bit-vector where each bit represents one enum constant, and EnumMap is a Map backed by an array indexed by enum ordinal, both far faster and smaller than their hash-based counterparts.
  • EnumSet – stores enum values as bits in a long (for enums with up to 64 constants) or a long[] (for larger enums); add, remove, and contains are single bitwise operations (AND, OR, XOR)
  • EnumMap – stores values in an Object[] array where the index is key.ordinal(); get and put are direct array accesses with no hashing
  • Memory – EnumSet uses 8 bytes (one long) for up to 64 constants versus hundreds of bytes for an equivalent HashSet; EnumMap uses one array slot per constant versus Node objects in HashMap
  • Factory methods – EnumSet provides allOf(), noneOf(), of(), range(), complementOf(), and copyOf() for concise creation
  • Iteration order – both iterate in enum declaration order (ordinal order), not insertion order

At National Bank of India, each transaction type (DEBIT, CREDIT, TRANSFER, FEE, INTEREST) is like a light switch on a panel. EnumSet is the panel itself: five switches, one per type. Turning on DEBIT and CREDIT means flipping two switches (setting two bits). Checking whether TRANSFER is active means glancing at one switch. The entire panel fits on a single chip (one long). In contrast, a HashSet is like building a separate filing cabinet with folders, hash buckets, and linked lists just to track which of five switches are on.

Understanding the Concepts

What are EnumSet and EnumMap?

EnumSet is a specialised Set implementation designed exclusively for enum types. Internally it represents the set as a bit-vector: each enum constant maps to one bit in a long value. Adding a constant sets its bit. Removing it clears the bit. Checking membership tests the bit. All operations are single bitwise instructions.

EnumMap is a specialised Map implementation where the key type must be an enum. Internally it stores values in a plain Object[] array indexed by key.ordinal(). There is no hashing, no bucket lookup, and no collision handling.

JAVA
package com.bank.demo;

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

public class EnumSetEnumMapDemo {
    enum TransactionType { DEBIT, CREDIT, TRANSFER, FEE, INTEREST }

    public static void main(String[] args) {
        // EnumSet
        Set<TransactionType> allowed = EnumSet.of(
            TransactionType.DEBIT, TransactionType.CREDIT, TransactionType.TRANSFER
        );
        System.out.println("Allowed: " + allowed);
        System.out.println("Contains FEE: " + allowed.contains(TransactionType.FEE));

        // EnumMap
        EnumMap<TransactionType, Integer> dailyCounts = new EnumMap<>(TransactionType.class);
        dailyCounts.put(TransactionType.DEBIT, 1520);
        dailyCounts.put(TransactionType.CREDIT, 890);
        dailyCounts.put(TransactionType.TRANSFER, 340);
        dailyCounts.put(TransactionType.FEE, 45);
        dailyCounts.put(TransactionType.INTEREST, 12);

        System.out.println("Daily counts: " + dailyCounts);
        System.out.println("Debit count: " + dailyCounts.get(TransactionType.DEBIT));
    }
}
TEXT
Allowed: [DEBIT, CREDIT, TRANSFER]
Contains FEE: false
Daily counts: {DEBIT=1520, CREDIT=890, TRANSFER=340, FEE=45, INTEREST=12}
Debit count: 1520

How does EnumSet work internally?

EnumSet has two internal implementations chosen automatically based on the number of constants in the enum.

  • RegularEnumSet (up to 64 constants) stores the entire set in a single long. Each constant's ordinal maps to one bit.
  • JumboEnumSet (more than 64 constants) uses a long[] array.

Operations map directly to bitwise instructions.

| Operation | Bitwise instruction | Time complexity |
|---|---|---|
| add(e) | elements \|= (1L << e.ordinal()) | O(1) |
| remove(e) | elements &= ~(1L << e.ordinal()) | O(1) |
| contains(e) | (elements & (1L << e.ordinal())) != 0 | O(1) |
| size() | Long.bitCount(elements) | O(1) |
| isEmpty() | elements == 0 | O(1) |

JAVA
package com.bank.demo;

import java.util.EnumSet;

public class EnumSetInternalsDemo {
    enum AccountStatus { ACTIVE, FROZEN, CLOSED, DORMANT, UNDER_REVIEW }

    public static void main(String[] args) {
        EnumSet<AccountStatus> statuses = EnumSet.noneOf(AccountStatus.class);
        // Internal long = 0b00000 (all bits off)

        statuses.add(AccountStatus.ACTIVE);       // 0b00001
        statuses.add(AccountStatus.UNDER_REVIEW); // 0b10001

        System.out.println("Set: " + statuses);
        System.out.println("Contains FROZEN: " + statuses.contains(AccountStatus.FROZEN));
        System.out.println("Size: " + statuses.size());

        // Factory methods
        EnumSet<AccountStatus> all = EnumSet.allOf(AccountStatus.class);
        EnumSet<AccountStatus> complement = EnumSet.complementOf(statuses);
        EnumSet<AccountStatus> range = EnumSet.range(AccountStatus.ACTIVE, AccountStatus.CLOSED);

        System.out.println("All: " + all);
        System.out.println("Complement: " + complement);
        System.out.println("Range ACTIVE..CLOSED: " + range);
    }
}
TEXT
Set: [ACTIVE, UNDER_REVIEW]
Contains FROZEN: false
Size: 2
All: [ACTIVE, FROZEN, CLOSED, DORMANT, UNDER_REVIEW]
Complement: [FROZEN, CLOSED, DORMANT]
Range ACTIVE..CLOSED: [ACTIVE, FROZEN, CLOSED]

How does EnumMap work internally?

EnumMap maintains an Object[] vals array of size equal to the number of enum constants. The index into this array is key.ordinal(). There is no hashing and no collision handling.

JAVA
package com.bank.demo;

import java.math.BigDecimal;
import java.util.EnumMap;
import java.util.Map;

public class EnumMapInternalsDemo {
    enum LoanType { HOME, PERSONAL, EDUCATION, VEHICLE, GOLD }

    public static void main(String[] args) {
        EnumMap<LoanType, BigDecimal> interestRates = new EnumMap<>(LoanType.class);
        interestRates.put(LoanType.HOME, new BigDecimal("8.50"));
        interestRates.put(LoanType.PERSONAL, new BigDecimal("12.75"));
        interestRates.put(LoanType.EDUCATION, new BigDecimal("7.25"));
        interestRates.put(LoanType.VEHICLE, new BigDecimal("9.50"));
        interestRates.put(LoanType.GOLD, new BigDecimal("10.00"));

        // Direct array access: vals[LoanType.HOME.ordinal()] = 8.50
        System.out.println("Home loan rate: " + interestRates.get(LoanType.HOME) + "%");

        // Iteration follows declaration order
        for (Map.Entry<LoanType, BigDecimal> entry : interestRates.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue() + "%");
        }
    }
}
TEXT
Home loan rate: 8.50%
HOME: 8.50%
PERSONAL: 12.75%
EDUCATION: 7.25%
VEHICLE: 9.50%
GOLD: 10.00%

How are EnumSet and EnumMap different from HashSet and HashMap?

| Feature | EnumSet | HashSet | EnumMap | HashMap |
|---|---|---|---|---|
| Backing structure | Bit-vector (long) | HashMap internally | Object[] array | Node[] with chains/trees |
| Hash computation | None (ordinal = bit position) | hashCode() | None (ordinal = index) | hashCode() + spread |
| Memory (5 entries) | 8 bytes (one long) | ~240 bytes (5 Nodes) | ~40 bytes (5 array slots) | ~400 bytes (5 Nodes + array) |
| contains/get speed | ~2 ns (bitwise AND) | ~20 ns (hash + equals) | ~2 ns (array access) | ~20 ns (hash + equals) |
| Iteration order | Enum declaration order | Unpredictable | Enum declaration order | Unpredictable |
| Null values | Not allowed | Allowed | Allowed | Allowed |

JAVA
package com.bank.demo;

import java.util.*;

public class EnumVsHashPerformanceDemo {
    enum Permission { READ, WRITE, DELETE, ADMIN, AUDIT }

    public static void main(String[] args) {
        // EnumSet: compact, fast
        Set<Permission> enumPerms = EnumSet.of(Permission.READ, Permission.WRITE);

        // HashSet: heavier
        Set<Permission> hashPerms = new HashSet<>(List.of(Permission.READ, Permission.WRITE));

        // Both behave the same functionally
        System.out.println("EnumSet contains READ: " + enumPerms.contains(Permission.READ));
        System.out.println("HashSet contains READ: " + hashPerms.contains(Permission.READ));

        // But EnumSet is far more efficient internally
        System.out.println("EnumSet class: " + enumPerms.getClass().getSimpleName());
    }
}
TEXT
EnumSet contains READ: true
HashSet contains READ: true
EnumSet class: RegularEnumSet

What are common mistakes?

Mistake: Using HashSet or HashMap for enum keys

JAVA
// WRONG: HashSet for enums is wasteful
Set<Permission> perms = new HashSet<>();
perms.add(Permission.READ);
perms.add(Permission.WRITE);
// Works, but wastes memory and CPU on hashing
JAVA
// CORRECT: EnumSet is purpose-built for enums
Set<Permission> perms = EnumSet.of(Permission.READ, Permission.WRITE);
// Single long, bitwise operations, 30x less memory

Mistake: Trying to create EnumSet with the constructor

JAVA
// WRONG: EnumSet has no public constructor
// EnumSet<Permission> perms = new EnumSet<>(); // COMPILE ERROR
JAVA
// CORRECT: Use factory methods
EnumSet<Permission> none = EnumSet.noneOf(Permission.class);
EnumSet<Permission> all = EnumSet.allOf(Permission.class);
EnumSet<Permission> some = EnumSet.of(Permission.READ, Permission.WRITE);

What are the edge cases?

JAVA
package com.bank.demo;

import java.util.EnumMap;
import java.util.EnumSet;

public class EnumEdgeCasesDemo {
    enum Empty {} // zero constants
    enum Single { ONLY }

    public static void main(String[] args) {
        // Edge case 1: EnumSet with single-constant enum
        EnumSet<Single> single = EnumSet.allOf(Single.class);
        System.out.println("Single-constant set: " + single);

        // Edge case 2: EnumMap allows null values but not null keys
        EnumMap<Single, String> map = new EnumMap<>(Single.class);
        map.put(Single.ONLY, null);
        System.out.println("Null value in EnumMap: " + map.get(Single.ONLY));

        // Edge case 3: EnumSet.noneOf still needs the enum class
        EnumSet<Single> empty = EnumSet.noneOf(Single.class);
        System.out.println("Empty EnumSet size: " + empty.size());
    }
}
TEXT
Single-constant set: [ONLY]
Null value in EnumMap: null
Empty EnumSet size: 0

What about thread safety?

EnumSet and EnumMap are not synchronised. For concurrent access, wrap them with Collections.synchronizedSet() or Collections.synchronizedMap(). Alternatively, use ConcurrentHashMap with enum keys if you need concurrent read-write access, though you lose the memory and speed benefits.

JAVA
package com.bank.demo;

import java.util.Collections;
import java.util.EnumMap;
import java.util.Map;
import java.util.Set;
import java.util.EnumSet;

public class SynchronizedEnumDemo {
    enum AccountType { SAVINGS, CURRENT, FIXED_DEPOSIT }

    public static void main(String[] args) {
        Set<AccountType> syncSet = Collections.synchronizedSet(
            EnumSet.of(AccountType.SAVINGS, AccountType.CURRENT)
        );
        Map<AccountType, String> syncMap = Collections.synchronizedMap(
            new EnumMap<>(AccountType.class)
        );
        syncMap.put(AccountType.SAVINGS, "Rajesh");
        System.out.println("Synchronized EnumMap: " + syncMap);
    }
}
TEXT
Synchronized EnumMap: {SAVINGS=Rajesh}

What should a senior engineer know?

  • Bit manipulation. EnumSet operations compile to single CPU instructions: OR for add, AND-NOT for remove, AND for contains. This is as fast as it gets on modern hardware.
  • EnumSet.allOf vs EnumSet.noneOf. For permission checks, start with noneOf and add specific permissions (whitelist) rather than allOf and remove (blacklist).
  • Serialization. EnumSet serializes by storing the enum class and the bit-vector. Adding new enum constants at the end is safe. Reordering constants changes ordinals and breaks serialized EnumSets.
  • EnumMap with streams. Use Collectors.groupingBy() with an EnumMap supplier for grouping:
JAVA
import java.util.EnumMap;
import java.util.List;
import java.util.stream.Collectors;

// Group transactions by type into an EnumMap
// transactions.stream().collect(
//     Collectors.groupingBy(Transaction::getType,
//         () -> new EnumMap<>(TransactionType.class),
//         Collectors.toList())
// );

What are common interview follow-up questions?

  • "Why is EnumSet faster than HashSet?" : EnumSet uses a bit-vector where each operation is a single bitwise instruction. No hashing, no equals comparison, no object allocation.
  • "Can EnumSet hold null?" : No. EnumSet does not permit null elements and throws NullPointerException.
  • "What happens if the enum has more than 64 constants?" : EnumSet uses JumboEnumSet backed by a long[] array.
  • "Does EnumMap maintain insertion order?" : No, it maintains enum declaration order (ordinal order).

Visual Overview

DIAGRAM
  EnumSet Internal Representation (RegularEnumSet)
  ================================================

  enum TransactionType { DEBIT, CREDIT, TRANSFER, FEE, INTEREST }
  ordinal:                  0       1        2      3       4

  EnumSet.of(DEBIT, CREDIT, FEE):
  long elements = 0b01011
                    ^  ^ ^
                    |  | └── DEBIT   (ordinal 0, bit 0)
                    |  └──── CREDIT  (ordinal 1, bit 1)
                    └────── FEE     (ordinal 3, bit 3)

  add(TRANSFER):  elements |= (1L << 2)  -> 0b01111
  remove(CREDIT): elements &= ~(1L << 1) -> 0b01101
  contains(FEE):  (elements & (1L << 3)) != 0 -> true

  EnumMap Internal Representation
  ===============================

  Object[] vals (size = enum constant count):
  ┌────────┬────────┬──────────┬──────┬──────────┐
  │ vals[0]│ vals[1]│  vals[2] │vals[3]│ vals[4] │
  │ 1520   │  890   │   340    │  45  │   12     │
  │ DEBIT  │ CREDIT │ TRANSFER │ FEE  │ INTEREST │
  └────────┴────────┴──────────┴──────┴──────────┘
  get(TRANSFER) -> vals[TRANSFER.ordinal()] -> vals[2] -> 340

Key Takeaway

EnumSet and EnumMap are purpose-built collection implementations for enum types that deliver dramatic performance and memory advantages over their general-purpose counterparts. EnumSet represents the entire set as a bit-vector in a single long value, where each enum constant occupies one bit. Add, remove, and contains are single bitwise operations that execute in nanoseconds with no hashing, no object allocation, and no collision handling.

EnumMap stores values in a plain array indexed by the enum constant's ordinal. A get or put is a direct array access. There are no hash computations, no bucket chains, and no tree promotions. Both collections iterate in enum declaration order, which is often the natural order for business logic.

In the National Bank of India's transaction processing system, Pankaj replaced all HashMap> groupings with EnumMap and all HashSet permission checks with EnumSet. The result was measurably lower memory usage and faster lookups in the hot path that processed thousands of transactions per second.

Remember one sentence: whenever your keys are enum constants, use EnumSet instead of HashSet and EnumMap instead of HashMap for bit-level speed and minimal memory.

---

Share
Use ← → arrow keys to navigate