跪拜 Guibai
← All articles
Source Code · Java · AI Programming

A CLI That Shows EnumMap's Internal State: Ordinal Index, NULL Sentinel, and Eager Allocation

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

EnumMap's ordinal-indexed storage explains its O(1) lookup without hash collision handling and its inability to support arbitrary keys. This CLI makes the design concrete, so developers understand when to choose EnumMap over HashMap and why null handling behaves the way it does.

Summary

Java's EnumMap stores values in a fixed Object[] whose index is the key enum's ordinal. There is no hashing, no resizing, and the array is allocated eagerly at construction. A new CLI demo makes this visible by reflecting into the private fields keyUniverse, vals, size, and the NULL sentinel, then printing the full slot-by-slot state after each operation.

The demo, generated with the help of Trae, shows that put with a null value stores a private sentinel object rather than null, so containsKey stays true while get returns null. It also demonstrates that remove and get return null for both missing keys and null-mapped keys, leaving size as the only way to tell them apart. clear simply fills the entire vals array with null and leaves the array length unchanged.

The tool requires JDK 16+ due to record and text blocks, and JDK 9+ module system restrictions mean it must run with --add-opens=java.base/java.util=ALL-UNNAMED. It works with five built-in enums (Planet, Vowel, Digit, Month, Direction) and accepts piped commands for reproducible output.

Takeaways
— EnumMap uses the key's ordinal as the direct index into a vals array; no hashing is involved.
— The vals array is eagerly allocated at construction, so put never triggers resizing.
— put(key, null) stores a private static NULL sentinel object, not null, to keep containsKey accurate.
— remove returns null for both missing keys and keys mapped to null; only the size change shows a removal happened.
— clear calls Arrays.fill(vals, null) and sets size to 0 while preserving the array length.
— The demo requires JDK 16+ (records, text blocks) and the --add-opens flag to reflect into java.util package.
— The CLI was generated with Trae, an AI programming assistant.
Conclusions

The NULL sentinel is a minimal way to distinguish 'no mapping' from 'mapped to null' without boxing, but it forces callers to check containsKey rather than relying on get.

Reflection-based demos like this turn JDK internals into a visual learning tool, but the module system restrictions (--add-opens) show why this approach isn't for production code.

EnumMap's eager allocation trades memory for speed: it always allocates an array as large as the enum's constant count, even if few keys are used.

The CLI's row-per-index output is a better fit than a GUI for displaying array structure because it preserves the ordering and alignment.

Concepts & terms
NULL sentinel
A private static sentinel object in EnumMap that replaces null when a key is explicitly mapped to null. This allows containsKey to distinguish 'no mapping' (vals[i] is null) from 'mapped to null' (vals[i] is the sentinel).
ordinal
The position of an enum constant in its declaration order, starting at 0. EnumMap uses it as the direct index into its internal value array.
--add-opens
A JVM flag that opens a package for deep reflection, required on JDK 9+ to access private fields in java.util for this demo.
maskNull/unmaskNull
Private methods that convert between null and the NULL sentinel when storing and retrieving values, ensuring get returns null for null-mapped keys.
From the discussion
Featured comments
卷土的土

The sentinel distinguishes null values from missing keys.

金銀銅鐵

Yes, that's right.

See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗