Definition
- EnumSet – stores enum values as bits in a
long(for enums with up to 64 constants) or along[](for larger enums); add, remove, and contains are single bitwise operations (AND, OR, XOR) - EnumMap – stores values in an
Object[]array where the index iskey.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(), andcopyOf()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.
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));
}
}
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) |
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);
}
}
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.
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() + "%");
}
}
}
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 |
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());
}
}
EnumSet contains READ: true
HashSet contains READ: true
EnumSet class: RegularEnumSet
What are common mistakes?
Mistake: Using HashSet or HashMap for enum keys
// 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
// 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
// WRONG: EnumSet has no public constructor
// EnumSet<Permission> perms = new EnumSet<>(); // COMPILE ERROR
// 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?
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());
}
}
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.
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);
}
}
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:
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
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.
---