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

EnumMap Internals, Visualized: The Ordinal-Indexed Array and NULL Sentinel

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

The NULL sentinel is why a key mapped to null still shows up in containsKey() but not in get(); without that distinction, code that checks existence after a null get() would silently misbehave. Understanding EnumMap's eager array allocation and ordinal indexing also makes the performance tradeoff against HashMap explicit.

Summary

EnumMap is one of the least hash-like maps in the Java collections framework: a key's ordinal is the array index, so lookups skip hashing, modulo, and collision handling entirely. A new Swing demo now makes that internal layout visible by reading EnumMap's private fields through reflection and drawing one cell per vals slot. The demo also exposes two behaviors that often surprise developers: put(key, null) stores a private static NULL sentinel rather than null, which is why get() can return null while containsKey(key) returns true; and remove() and clear() write null back into the slots without shrinking the array. The visual grid turns gray for absent keys, blue for real values, and orange for the NULL sentinel, with an amber border marking the slot touched by the last operation. The full Java source runs on JDK 16+ and requires --add-opens=java.base/java.util=ALL-UNNAMED to bypass the module system when reflecting into EnumMap's private fields.

Takeaways
— EnumMap stores values in an Object[] vals indexed by the enum constant's ordinal, so there is no hashing, modulo, or collision handling.
— The constructor eagerly allocates vals to the exact size of the enum constant count; the array never shrinks on clear().
— put(key, null) stores a private static NULL sentinel object, not null, so that 'no mapping' (slot == null) and 'mapped to null' (slot == NULL) stay distinguishable.
— get() uses unmaskNull to convert the sentinel back to null, while containsKey() only checks whether the slot is non-null, which is why get can return null and containsKey still returns true.
— remove() sets the slot to null; removing an already-absent key is a no-op and leaves size unchanged.
— clear() performs Arrays.fill(vals, null) and sets size = 0, but the vals array length is preserved.
— Reflection access to EnumMap's private fields requires --add-opens=java.base/java.util=ALL-UNNAMED on JDK 9+.
— The demo code uses records, instanceof pattern matching, and text blocks, so it needs JDK 16+.
Conclusions

The eager allocation of vals means EnumMap's memory footprint is fixed at construction time, proportional to the enum constant count; for enums with many constants or many short-lived maps, that can be wasteful compared to HashMap's lazy table growth.

The NULL sentinel pattern is a clean way to support null values in a structure that otherwise uses null to signal absence; any developer relying on EnumMap for nullable values needs to remember that get() and containsKey() answer different questions.

Reading private fields with reflection is fine for an educational demo, but the module system's --add-opens requirement is a reminder that these internals are not a stable API and could change in future JDK versions.

Concepts & terms
EnumMap
A specialized Map implementation in java.util that uses the ordinal of an enum constant as the direct index into a parallel value array, eliminating hashing and collisions.
Ordinal
The zero-based position of a constant in its enum declaration. EnumMap uses it as the array index for both the key universe and the value array.
NULL sentinel
A private static Object in EnumMap that stands in for null values. It lets the map distinguish between an absent key (slot is null) and a key mapped to null (slot is the sentinel).
maskNull / unmaskNull
Internal EnumMap helpers that convert null to the NULL sentinel (mask) and back (unmask) when reading values.
Eager allocation
In EnumMap, the vals array is sized and filled with null as soon as the constructor runs, rather than growing lazily like ArrayList's empty constructor.
From the discussion
Featured comments
亚雷

You could add a comparison of the state after removing a null-value mapping.

金銀銅鐵

Okay, adjusted.

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