跪拜 Guibai
← All articles
Backend · AI Programming · Java

Watch a Single long Become an EnumSet: A GUI for RegularEnumSet Internals

By 金銀銅鐵 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

The bitmask design explains why EnumSet is compact and fast: membership tests are bit reads, mutations are OR and AND-NOT, and the 64-constant cutoff is exactly where one long stops being enough. Seeing the bits move makes that internal tradeoff concrete and makes JVM quirks like --add-opens for reflection visible at the same time.

Summary

Java's EnumSet does not use a conventional hash table for small enums. For types with up to 64 constants, RegularEnumSet stores membership in one private long field named elements, using the enum constant's ordinal as a bit index. This GUI demo, built with Swing and generated with Trae, reads that field through reflection and redraws a bit grid after every operation. Users pick one of five enum types, invoke a factory or mutation method, and watch the same state appear in decimal, hex, binary, and per-bit cells.

The left panel also reveals that size is not stored separately; it is computed as Long.bitCount(elements). The operation log records each call, the before and after bitmask, and the exact bit operation: add is elements |= 1L << ordinal, remove is elements &= ~(1L << ordinal), clear zeroes the field. Invalid range calls, where from.ordinal() exceeds to.ordinal(), produce an IllegalArgumentException, a red banner, and a red border around the offending controls. The demo requires --add-opens=java.base/java.util=ALL-UNNAMED on JDK 9+ and uses record syntax, so it needs JDK 16+.

Takeaways
— EnumSet.noneOf returns a RegularEnumSet when the enum has <= 64 constants and a JumboEnumSet when it has > 64.
— RegularEnumSet keeps membership in a single private long field named elements; bit i is 1 exactly when the constant with ordinal i is in the set.
— noneOf sets elements to 0 immediately, while allOf sets it to (1L << n) - 1.
— add(e) performs elements |= (1L << e.ordinal()), and adding an existing member is a no-op.
— remove(e) performs elements &= ~(1L << e.ordinal()); only the target bit is cleared.
— clear() sets elements to 0.
— RegularEnumSet.size() is Long.bitCount(elements), with no separate counter.
— EnumSet.range(from, to) throws IllegalArgumentException when from.ordinal() > to.ordinal().
— Reflecting on elements on JDK 9+ requires java --add-opens=java.base/java.util=ALL-UNNAMED; the source uses record syntax, so it needs JDK 16+.
— Choosing an enum type or clicking Reset clears the previous set and error state.
Conclusions

A single long is enough for any enum up to 64 constants, so RegularEnumSet keeps the whole collection in one CPU word; JumboEnumSet exists only because one long stops being enough beyond that.

Logging each operation as a bit expression exposes no-op calls clearly: re-adding an existing constant leaves elements unchanged and the UI does not highlight any bit.

The JDK 9 module system makes reaching this private field a deliberate act: the --add-opens flag is required even for a teaching demo.

Concepts & terms
RegularEnumSet
An EnumSet implementation for enum types with 64 or fewer constants. It stores membership in a single private long field named elements, where bit i records whether the constant with ordinal i is in the set.
JumboEnumSet
An EnumSet implementation for enum types with more than 64 constants. It uses a long[] array instead of a single long to hold the membership bitmask.
ordinal
The zero-based position of an enum constant in its declaration order. EnumSet uses this value as the bit index inside the elements bitmask.
bitmask
A value in which individual bits represent the presence or absence of items. In RegularEnumSet, bit i of elements is 1 exactly when the constant with ordinal i is a member.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗