跪拜 Guibai
← Back to the summary

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

Background

In previous articles we used a GUI tool to observe java.util.EnumSet:

In those articles, ordinal was used to locate a bit. java.util.EnumMap follows the same idea but uses it more directly — ordinal is the array index.

Save the following code as EnumMapFields.java:

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class EnumMapFields {
    public static void main(String[] args) {
        Class<?> clazz = java.util.EnumMap.class;
        Field[] fields = clazz.getDeclaredFields();

        System.out.println(clazz.getName() + " declares " + fields.length + " fields:");
        System.out.printf("%-25s %-45s %s%n", "modifiers", "type", "name");
        System.out.println("-".repeat(80));

        for (Field field : fields) {
            String modifiers = Modifier.toString(field.getModifiers());
            String type = field.getGenericType().getTypeName();
            System.out.printf("%-25s %-45s %s%n", modifiers, type, field.getName());
        }
    }
}

Compile and run the main method with these commands (I used JDK 21):

javac EnumMapFields.java
java EnumMapFields

Output:

java.util.EnumMap declares 7 fields:
modifiers                 type                                          name
--------------------------------------------------------------------------------
private final             java.lang.Class<K>                            keyType
private transient         K[]                                           keyUniverse
private transient         java.lang.Object[]                            vals
private transient         int                                           size
private static final      java.lang.Object                              NULL
private transient         java.util.Set<java.util.Map$Entry<K, V>>      entrySet
private static final      long                                          serialVersionUID

We only care about the first 5 fields. Their positions in the source are shown below:

image.png

Observable facts for these five fields:

The constructor logic is straightforward:

image.png

The constructor allocates space for vals.

Comparing with EnumSet, the use of ordinal differs:

Bitmap Index
EnumSet ordinal locates a bit in long/long[] —
EnumMap — ordinal directly as vals array index

We can use reflection to observe how keyUniverse, vals, and size change when adding or removing key-value pairs in java.util.EnumMap.

Notes

Body

I used trae to generate code that lets you observe the internal state of java.util.EnumMap. For readability, the complete code is placed in the complete code section.

How to run

Save the complete code as EnumMapBehaviorDemo.java. Compile it with:

javac EnumMapBehaviorDemo.java

After compilation you get the following class files:

Run the main method with:

java --add-opens=java.base/java.util=ALL-UNNAMED EnumMapBehaviorDemo

Example output

Initial state

image.png

The controls have five rows; each row has a small caption on the left: constructor and java API are blue to match the corresponding Java API buttons; demo config and demo only are gray to indicate demo helper functionality:

Blue buttons correspond to real Java API calls; dark gray indicates demo helper controls; light gray indicates currently unclickable.

Unlike the EnumSet demo, the first row here is not "choose enum type" but "choose the enum type for the key". Because EnumMap has a second type parameter V (the value type), and the demo fixes all values to String.

Before the constructor is called, put / get / containsKey / remove / clear / Reset are all disabled. At that point the map does not exist, the state panel shows --, and all grid cells show ?.

The internal state of the EnumMap panel

The internal state of the EnumMap panel in the middle shows the current value of each field; it refreshes after every operation. For example, clicking the new EnumMap<>(天干.class) button makes the panel show 5 lines:

keyType            = 天干.class
keyUniverse.length = 10
vals.length        = 10
size               = 0
map                = {}

The grid

image.png

The grid has one row; each cell corresponds to an index in the vals array. The text inside each cell:

Below each cell the index is displayed.

Cell background colors distinguish three cases, plus an amber border for the cell touched by the most recent operation:

Calling the constructor

After clicking the new EnumMap<>(五行.class) button, the log appends:

new EnumMap<>(五行.class)                  vals = new Object[5], all null [allocated eagerly: ordinal IS the index, no hashing]

vals is immediately allocated as an array of length 5 (五行 has 5 constants), all slots are null, and size is 0. From this row onward the grid shows actual content: all ? become empty slots (5 light gray cells).

This also demonstrates the role of keyType: EnumMap must know which enum it uses in order to obtain keyUniverse and thus determine the length of vals.

Adding a key-value pair: put(key, value)

Select 五行 in the key enum dropdown, choose 金 in the key dropdown, enter 钱 in the value field, then click put(五行.金, "钱"):

image.png

The log appends:

put(五行.金, "钱")                           vals[0] null -> "钱" [size 0 -> 1]

金 has ordinal 0, so the value is written directly into vals[0], and size changes from 0 to 1. There is no hashing, no modulo, and no collision handling here. Compared with HashMap's "perturb + locate + linked list", the locating cost is almost negligible.

Calling put again with the same key overwrites the value but size stays unchanged:

put(五行.金, "镇")                           vals[0] "钱" -> "镇" [slot was occupied: size unchanged (1)]

This corresponds to the put method in the JDK source:

image.png

Only when the previous slot is null does size increment by 1.

Adding a key-value pair: put(key, null)

Check the null checkbox and execute put(五行.水, null):

image.png

The cell turns orange, and the log reads:

put(五行.水, null)                          vals[2] null -> NULL [maskNull: null stored as NULL sentinel; size 1 -> 2]

This is a confusing detail in EnumMap: put(key, null) does not store null; it stores the private NULL sentinel object. The maskNull / unmaskNull methods perform this conversion:

image.png

The distinction exists because vals[i] == null must mean "this key has no mapping", while "mapped to null" needs a distinguishable marker. Otherwise containsKey would be impossible to implement. (This is like the difference between "I have an empty wallet" versus "I don't even have a wallet".)

get and containsKey

Execute get and containsKey on the same key (currently mapped to null):

image.png

Logs:

get(五行.水)                                returns null [unmaskNull(NULL) -> null: looks absent, but containsKey(水) is true]
containsKey(五行.水)                        true [vals[2] != null  ->  true  (the NULL sentinel counts as present)]

The two results look contradictory but are actually consistent:

This also explains why EnumMap cannot simply store null in vals: otherwise it could not distinguish "this key is absent" from "the key's value is null".

Removing a key-value pair: remove

Execute remove(五行.金); the cell changes from blue to light gray (I launched two demos side by side for the screenshot):

image.png

remove(五行.金)                             vals[0] "钱" -> null [size 1 -> 0]

If a key's value is null and we remove such a key, the cell changes from orange to light gray (two demos running):

image.png

Relevant logs:

new EnumMap<>(五行.class)                  vals = new Object[5], all null [allocated eagerly: ordinal IS the index, no hashing]
put(五行.金, "钱")                           vals[0] null -> "钱" [size 0 -> 1]
put(五行.土, null)                          vals[4] null -> NULL [maskNull: null stored as NULL sentinel; size 1 -> 2]
remove(五行.土)                             vals[4] NULL -> null [size 2 -> 1]

If remove is called on a key that has no mapping, nothing changes (this case is simple enough to skip a screenshot):

new EnumMap<>(五行.class)                  vals = new Object[5], all null [allocated eagerly: ordinal IS the index, no hashing]
put(五行.金, "钱")                           vals[0] null -> "钱" [size 0 -> 1]
remove(五行.水)                             [no-op: vals[2] was already null, size unchanged]

This rule is consistent with previous demos: highlighting must align with actual data changes, and no-op operations get no highlight to avoid misleading.

The clear() button

Clicking clear() empties all slots:

clear()                                  [Arrays.fill(vals, null): all 5 slots cleared, size = 0]

The source does Arrays.fill(vals, null) plus size = 0. Note that the length of vals does not change — clear only fills slots with null, the array itself is retained, and the next put does not require reallocation.

image.png

The Operation log panel

The Operation log panel at the bottom records every operation. Startup logs:

image.png

--- enum type: Planet (the eight planets) ---
put/get/containsKey/remove/clear are disabled until new EnumMap<>() runs
this demo helps you observe:
  new EnumMap<>(X.class) -> vals = new Object[8], all null (eager, no lazy init)
  put(k, v)              -> vals[k.ordinal()] = v     (ordinal IS the index: no hashing)
  put(k, null)           -> vals[k.ordinal()] = NULL  (maskNull: sentinel, containsKey stays true)
  get(k)                 -> unmaskNull(vals[k.ordinal()]): null both when absent AND when mapped to null
  remove(k)              -> vals[k.ordinal()] = null
  clear()                -> Arrays.fill(vals, null), size = 0
grid: gray = null slot, blue = value, orange = NULL sentinel, amber border = last touched

Thereafter each operation appends a line and auto-scrolls to the newest one. Each line has three columns:

  1. The expression of that call, e.g., put(五行.金, "钱")
  2. The touched slot and its change, written as "slot: before -> after"; if no slot changed, only a note appears without ->
  3. A bracketed note explaining what happened to vals and size

Using an earlier line as example:

put(五行.金, "钱")                           vals[0] null -> "钱" [size 0 -> 1]

The Reset button

Reset is a demo-helper button, not a Java API. Clicking it discards the current map, clears the Operation log, and outputs a startup message beginning with --- demo reset: call the constructor first ---; the controls return to the initial state (constructor enabled, other buttons disabled again).

Complete code

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.lang.reflect.Field;
import java.lang.reflect.InaccessibleObjectException;
import java.util.EnumMap;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.border.TitledBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

/**
 * Swing demo in the style of ArrayListBehaviorDemo and
 * RegularEnumSetBehaviorDemo: it visualizes the internal state of
 * java.util.EnumMap while operations are executed.
 * <p>
 * EnumMap keeps two parallel arrays: "keyUniverse" holds every constant of
 * the key enum, and "vals" holds the associated values. A key's ordinal is
 * used DIRECTLY as the array index - there is no hashing at all. The enum
 * class itself is stored in the final field "keyType" at construction and
 * never changes afterwards. A private static sentinel object NULL stands in
 * for null values, so "no mapping" (slot == null) and "mapped to null"
 * (slot == NULL) stay distinguishable. The demo reads all of these private
 * fields via reflection and draws one cell per vals slot.
 * <p>
 * Observable facts:
 * 1) new EnumMap<>(X.class) -> keyType = X.class (final), keyUniverse = all
 *    constants of X, vals = new Object[n], all null, size = 0 (allocated
 *    eagerly: no lazy init like ArrayList's empty-arg constructor);
 * 2) put(k, v)    -> vals[k.ordinal()] = v (ordinal IS the index: no hashing);
 * 3) put(k, null) -> vals[k.ordinal()] = NULL (maskNull: the sentinel keeps
 *    containsKey true even though get(k) returns null);
 * 4) remove(k)    -> vals[k.ordinal()] = null;
 * 5) clear()      -> Arrays.fill(vals, null), and size = 0;
 * 6) size is a maintained field.
 * <p>
 * The fields are private, so reflection is required. The demo uses records,
 * instanceof patterns and text blocks, so JDK 16+ is needed:
 * javac EnumMapBehaviorDemo.java
 * java --add-opens=java.base/java.util=ALL-UNNAMED EnumMapBehaviorDemo
 */
public class EnumMapBehaviorDemo {

    // ---- selectable enum universes (same five as RegularEnumSetBehaviorDemo) ----

    /** The eight planets (default demo enum). */
    private enum Planet { MERCURY, VENUS, EARTH, MARS, JUPITER, SATURN, URANUS, NEPTUNE }

    /** 金木水火土: metal, wood, water, fire, earth. */
    private enum 五行 { 金, 木, 水, 火, 土 }

    /** 甲乙丙丁戊己庚辛壬癸: the ten heavenly stems. */
    private enum 天干 { 甲, 乙, 丙, 丁, 戊, 己, 庚, 辛, 壬, 癸 }

    /** 子丑寅卯辰巳午未申酉戌亥: the twelve earthly branches. */
    private enum 地支 { 子, 丑, 寅, 卯, 辰, 巳, 午, 未, 申, 酉, 戌, 亥 }

    /** 东西南北: east, south, west, north. */
    private enum 方位 { 东, 南, 西, 北 }

    /** One selectable entry of the "enum type" combo box. */
    private record DemoEnumType(String displayName, String gloss, Class<? extends Enum<?>> type) {
        @Override
        public String toString() {
            return displayName;
        }
    }

    private static final List<DemoEnumType> DEMO_ENUMS = List.of(
            new DemoEnumType("Planet", "the eight planets", Planet.class),
            new DemoEnumType("五行", "金木水火土, the five phases", 五行.class),
            new DemoEnumType("天干", "甲乙丙丁戊己庚辛壬癸, the ten heavenly stems", 天干.class),
            new DemoEnumType("地支", "子丑寅卯辰巳午未申酉戌亥, the twelve earthly branches", 地支.class),
            new DemoEnumType("方位", "东西南北, the four cardinal directions", 方位.class));

    // ---- shared design language with the other behavior demos ----
    /** Blue for buttons that map 1:1 to Java code (IDE syntax-highlight blue). */
    private static final Color CODE_COLOR = new Color(21, 101, 192);
    /** Dark gray for demo helper buttons: clearly secondary, not disabled. */
    private static final Color DEMO_COLOR = new Color(93, 93, 93);
    /** Light gray used when a button is disabled. */
    private static final Color DISABLED_COLOR = new Color(175, 175, 175);

    /** Slot holds no mapping (vals[i] == null). */
    private static final Color SLOT_EMPTY_COLOR = new Color(240, 240, 240);
    /** Slot holds a real value. */
    private static final Color SLOT_VALUE_COLOR = new Color(66, 133, 244);
    /** Slot holds the NULL sentinel: the key is mapped to null. */
    private static final Color SLOT_NULL_COLOR = new Color(251, 140, 0);
    /** Border of the slot touched by the most recent operation. */
    private static final Color LAST_TOUCHED_COLOR = new Color(244, 180, 0);

    private static final Font CODE_FONT = new Font(Font.MONOSPACED, Font.BOLD, 13);

    // ---- reflection on java.util.EnumMap ----
    private final Field keyTypeField;
    private final Field keyUniverseField;
    private final Field valsField;
    private final Field sizeField;
    /** EnumMap's private static sentinel: stored in vals[] instead of null. */
    private final Object nullSentinel;

    // ---- state ----
    /** null means "no map yet": operations are disabled until the constructor runs. */
    private EnumMap<?, String> map;
    private int lastTouchedIndex = -1;

    // ---- UI ----
    private JFrame frame;
    private JButton ctorButton;
    private JButton putButton;
    private JButton getButton;
    private JButton containsKeyButton;
    private JButton removeButton;
    private JButton clearButton;
    private JButton resetButton;
    private JComboBox<DemoEnumType> enumTypeCombo;
    private JComboBox<Object> keyCombo;
    private JTextField valueField;
    private JCheckBox nullCheck;
    private JLabel glossLabel;

    private JLabel keyTypeLabel;
    private JLabel keyUniverseLabel;
    private JLabel valsLengthLabel;
    private JLabel sizeLabel;
    private JLabel mapLabel;
    private JTextArea logArea;
    private ValsGrid valsGrid;

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            EnumMapBehaviorDemo demo;
            try {
                demo = new EnumMapBehaviorDemo();
            } catch (InaccessibleObjectException e) {
                JOptionPane.showMessageDialog(null,
                        """
                        Reflection access to EnumMap internals was denied.

                        On JDK 9+ start the demo with:
                        java --add-opens=java.base/java.util=ALL-UNNAMED \\
                            EnumMapBehaviorDemo""",
                        "--add-opens required", JOptionPane.ERROR_MESSAGE);
                return;
            } catch (ReflectiveOperationException e) {
                JOptionPane.showMessageDialog(null,
                        "Initialization failed: " + e.getMessage(),
                        "Error", JOptionPane.ERROR_MESSAGE);
                return;
            }
            demo.frame.setVisible(true);
        });
    }

    public EnumMapBehaviorDemo() throws ReflectiveOperationException {
        keyTypeField = EnumMap.class.getDeclaredField("keyType");
        keyUniverseField = EnumMap.class.getDeclaredField("keyUniverse");
        valsField = EnumMap.class.getDeclaredField("vals");
        sizeField = EnumMap.class.getDeclaredField("size");
        Field nullField = EnumMap.class.getDeclaredField("NULL");
        keyTypeField.setAccessible(true);
        keyUniverseField.setAccessible(true);
        valsField.setAccessible(true);
        sizeField.setAccessible(true);
        nullField.setAccessible(true);
        nullSentinel = nullField.get(null);
        createAndShowGui();
    }

    private void createAndShowGui() {
        // Snappier, longer-lived tooltips (same tuning as the other demos).
        ToolTipManager.sharedInstance().setInitialDelay(300);
        ToolTipManager.sharedInstance().setDismissDelay(12000);

        frame = new JFrame("EnumMap internals demo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLayout(new BorderLayout());

        JPanel control = new JPanel();
        control.setLayout(new BoxLayout(control, BoxLayout.Y_AXIS));
        control.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));

        // ---------- row 0: which enum to demo ----------
        JPanel typeRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
        typeRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        enumTypeCombo = new JComboBox<>();
        for (DemoEnumType t : DEMO_ENUMS) {
            enumTypeCombo.addItem(t);
        }
        enumTypeCombo.setToolTipText("Chooses which enum class the demo operates on");
        glossLabel = new JLabel();

        JPanel typeGroup = new JPanel();
        typeGroup.setBorder(BorderFactory.createEtchedBorder());
        typeGroup.add(new JLabel("key enum ="));
        typeGroup.add(enumTypeCombo);
        typeGroup.add(glossLabel);

        typeRow.add(rowCaption("demo config", DEMO_COLOR));
        typeRow.add(typeGroup);
        control.add(typeRow);

        // ---------- row 1: constructor ----------
        JPanel ctorRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
        ctorRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        ctorButton = new JButton();
        ctorButton.setFont(CODE_FONT);
        updateCtorButtonText();
        ctorRow.add(rowCaption("constructor", CODE_COLOR));
        ctorRow.add(ctorButton);
        control.add(ctorRow);

        // ---------- row 2: put ----------
        JPanel putRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
        putRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        keyCombo = new JComboBox<>();
        for (Enum<?> c : currentConstants()) {
            keyCombo.addItem(c);
        }
        valueField = new JTextField("value", 8);
        nullCheck = new JCheckBox("null");
        nullCheck.setToolTipText("Check to call put(key, null): "
                + "the slot then holds the NULL sentinel instead of null");
        putButton = new JButton();
        putButton.setFont(CODE_FONT);
        putButton.setToolTipText("Runs: map.put(key, value) with the controls on the left");

        // The put/get/containsKey/remove buttons show the exact call for the
        // current controls (set at the end of createAndShowGui, once all four
        // buttons exist - switchEnumType() calls updateOpButtonTexts()).
        keyCombo.addActionListener(e -> updateOpButtonTexts());
        nullCheck.addActionListener(e -> {
            valueField.setEnabled(!nullCheck.isSelected() && map != null);
            updateOpButtonTexts();
        });
        valueField.getDocument().addDocumentListener(new DocumentListener() {
            @Override public void insertUpdate(DocumentEvent e) { updateOpButtonTexts(); }
            @Override public void removeUpdate(DocumentEvent e) { updateOpButtonTexts(); }
            @Override public void changedUpdate(DocumentEvent e) { updateOpButtonTexts(); }
        });

        // Group the button with its two arguments so the three controls read
        // as one unit (same pattern as the range group in the EnumSet demos).
        JPanel putGroup = new JPanel();
        putGroup.setBorder(BorderFactory.createEtchedBorder());
        putGroup.add(new JLabel("key ="));
        putGroup.add(keyCombo);
        putGroup.add(new JLabel("value ="));
        putGroup.add(valueField);
        putGroup.add(nullCheck);
        putGroup.add(putButton);

        putRow.add(rowCaption("java API", CODE_COLOR));
        putRow.add(putGroup);
        control.add(putRow);

        // ---------- row 3: read/remove operations ----------
        JPanel opRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
        opRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        getButton = new JButton();
        containsKeyButton = new JButton();
        removeButton = new JButton();
        clearButton = new JButton("clear()");
        getButton.setFont(CODE_FONT);
        containsKeyButton.setFont(CODE_FONT);
        removeButton.setFont(CODE_FONT);
        clearButton.setFont(CODE_FONT);
        getButton.setToolTipText("Runs: map.get(key) - the return value is logged");
        containsKeyButton.setToolTipText("Runs: map.containsKey(key) - the result is logged");
        removeButton.setToolTipText("Runs: map.remove(key)");
        clearButton.setToolTipText("Runs: map.clear()");

        opRow.add(rowCaption("java API", CODE_COLOR));
        opRow.add(getButton);
        opRow.add(containsKeyButton);
        opRow.add(removeButton);
        opRow.add(clearButton);
        control.add(opRow);

        // ---------- row 4: demo-only helpers ----------
        JPanel demoRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
        demoRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        resetButton = new JButton("Reset");
        resetButton.setToolTipText("Demo control, not a Java API call: returns to the initial state");
        demoRow.add(rowCaption("demo only", DEMO_COLOR));
        demoRow.add(resetButton);
        control.add(demoRow);

        // ---------- state panel: one labeled line per internal fact ----------
        JPanel statePanel = new JPanel();
        statePanel.setLayout(new BoxLayout(statePanel, BoxLayout.Y_AXIS));
        statePanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        statePanel.setBorder(new TitledBorder("internal state of the EnumMap"));

        keyTypeLabel = new JLabel();
        keyUniverseLabel = new JLabel();
        valsLengthLabel = new JLabel();
        sizeLabel = new JLabel();
        mapLabel = new JLabel();
        for (JLabel line : new JLabel[]{keyTypeLabel, keyUniverseLabel,
                valsLengthLabel, sizeLabel, mapLabel}) {
            line.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
            JPanel row = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 2));
            row.setAlignmentX(Component.LEFT_ALIGNMENT);
            row.add(line);
            statePanel.add(row);
        }

        valsGrid = new ValsGrid();
        statePanel.add(valsGrid);
        frame.add(statePanel, BorderLayout.CENTER);

        // ---------- operation log ----------
        logArea = new JTextArea();
        logArea.setEditable(false);
        logArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
        JScrollPane logScroll = new JScrollPane(logArea);
        logScroll.setBorder(new TitledBorder("Operation log"));
        logScroll.setPreferredSize(new Dimension(0, 180));

        frame.add(control, BorderLayout.NORTH);
        frame.add(logScroll, BorderLayout.SOUTH);

        // ---------- listeners ----------
        enumTypeCombo.addActionListener(e -> switchEnumType());
        ctorButton.addActionListener(e -> doConstructor());
        putButton.addActionListener(e -> doPut());
        getButton.addActionListener(e -> doGet());
        containsKeyButton.addActionListener(e -> doContainsKey());
        removeButton.addActionListener(e -> doRemove());
        clearButton.addActionListener(e -> doClear());
        resetButton.addActionListener(e -> resetDemo());

        switchEnumType();

        frame.setSize(980, 720);
        frame.setLocationRelativeTo(null);
    }

    // =====================================================================
    // Operations (each logs the before/after of the touched vals slot)
    // =====================================================================

    private void doConstructor() {
        map = newEnumMapRaw(currentType());
        lastTouchedIndex = -1;
        setMapCreatedState();
        int n = currentConstants().length;
        logLine(String.format("%-40s %s", "new EnumMap<>(" + currentTypeName() + ".class)",
                "vals = new Object[" + n + "], all null"
                        + " [allocated eagerly: ordinal IS the index, no hashing]"));
        refreshView();
    }

    private void doPut() {
        Enum<?> key = selectedConstant();
        boolean putNull = nullCheck.isSelected();
        String value = putNull ? null : valueField.getText();
        int idx = key.ordinal();
        Object old = getVals()[idx];
        boolean wasPresent = old != null;
        int oldSize = getSize();

        putRaw(map, key, value);

        Object now = getVals()[idx];
        lastTouchedIndex = idx;
        String note;
        if (wasPresent) {
            note = "slot was occupied: size unchanged (" + getSize() + ")";
        } else if (putNull) {
            note = "maskNull: null stored as NULL sentinel; size " + oldSize + " -> " + getSize();
        } else {
            note = "size " + oldSize + " -> " + getSize();
        }
        logLine(String.format("%-40s vals[%d] %s -> %s [%s]", putText(key, value),
                idx, slotText(old), slotText(now), note));
        refreshView();
    }

    private void doGet() {
        Enum<?> key = selectedConstant();
        int idx = key.ordinal();
        String result = map.get(key);
        Object slot = getVals()[idx];
        lastTouchedIndex = idx;
        String note;
        if (slot == nullSentinel) {
            note = "unmaskNull(NULL) -> null: looks absent, but containsKey("
                    + key.name() + ") is true";
        } else if (slot == null) {
            note = "vals[" + idx + "] is null: no mapping";
        } else {
            note = "unmaskNull(vals[" + idx + "])";
        }
        logLine(String.format("%-40s returns %s [%s]", callText("get", key),
                result == null ? "null" : "\"" + result + "\"", note));
        refreshView();
    }

    private void doContainsKey() {
        Enum<?> key = selectedConstant();
        int idx = key.ordinal();
        boolean result = map.containsKey(key);
        lastTouchedIndex = idx;
        Object slot = getVals()[idx];
        String note = "vals[" + idx + "] != null  ->  " + result;
        if (slot == nullSentinel) {
            note += "  (the NULL sentinel counts as present)";
        }
        logLine(String.format("%-40s %s [%s]", callText("containsKey", key), result, note));
        refreshView();
    }

    private void doRemove() {
        Enum<?> key = selectedConstant();
        int idx = key.ordinal();
        Object old = getVals()[idx];
        int oldSize = getSize();

        map.remove(key);

        if (old == null) {
            // align the highlight with actual data change: a no-op remove
            // touches nothing, so no cell is marked
            logLine(String.format("%-40s [no-op: vals[%d] was already null, size unchanged]",
                    callText("remove", key), idx));
        } else {
            lastTouchedIndex = idx;
            logLine(String.format("%-40s vals[%d] %s -> null [size %d -> %d]",
                    callText("remove", key), idx, slotText(old), oldSize, getSize()));
        }
        refreshView();
    }

    private void doClear() {
        map.clear();
        lastTouchedIndex = -1;
        int n = currentConstants().length;
        logLine(String.format("%-40s [Arrays.fill(vals, null): all %d slots cleared, size = 0]",
                "clear()", n));
        refreshView();
    }

    // =====================================================================
    // UI state
    // =====================================================================

    /** Demo helper: drops the map and returns every control to the start. */
    private void resetDemo() {
        map = null;
        lastTouchedIndex = -1;
        setNoMapState();
        logArea.setText("");
        printStartupLog(true);
        refreshView();
    }

    private void switchEnumType() {
        map = null;
        lastTouchedIndex = -1;
        DemoEnumType t = currentEnumType();

        keyCombo.removeAllItems();
        for (Enum<?> c : currentConstants()) {
            keyCombo.addItem(c);
        }
        glossLabel.setText("(" + t.gloss() + ")");
        updateCtorButtonText();
        updateOpButtonTexts();
        setNoMapState();
        logArea.setText("");
        printStartupLog(false);
        refreshView();
    }

    /** Button state before the constructor runs (also right after Reset). */
    private void setNoMapState() {
        ctorButton.setEnabled(true);
        keyCombo.setEnabled(false);
        valueField.setEnabled(false);
        nullCheck.setEnabled(false);
        putButton.setEnabled(false);
        getButton.setEnabled(false);
        containsKeyButton.setEnabled(false);
        removeButton.setEnabled(false);
        clearButton.setEnabled(false);
        resetButton.setEnabled(false);
        refreshButtonColors();
    }

    /** Button state once the map exists. */
    private void setMapCreatedState() {
        ctorButton.setEnabled(false);
        keyCombo.setEnabled(true);
        valueField.setEnabled(!nullCheck.isSelected());
        nullCheck.setEnabled(true);
        putButton.setEnabled(true);
        getButton.setEnabled(true);
        containsKeyButton.setEnabled(true);
        removeButton.setEnabled(true);
        clearButton.setEnabled(true);
        resetButton.setEnabled(true);
        refreshButtonColors();
    }

    /** Enabled Java-API buttons are blue, demo helpers dark gray. */
    private void refreshButtonColors() {
        JButton[] codeButtons = {ctorButton, putButton, getButton,
                containsKeyButton, removeButton, clearButton};
        for (JButton b : codeButtons) {
            b.setForeground(b.isEnabled() ? CODE_COLOR : DISABLED_COLOR);
        }
        resetButton.setForeground(resetButton.isEnabled() ? DEMO_COLOR : DISABLED_COLOR);
    }

    private void updateCtorButtonText() {
        String text = "new EnumMap<>(" + currentTypeName() + ".class)";
        ctorButton.setText(text);
        ctorButton.setToolTipText("Runs: " + text);
    }

    /** Shows the exact call for the currently selected key and value. */
    private void updateOpButtonTexts() {
        Enum<?> key = selectedConstant();
        if (key == null) {
            return;
        }
        String value = nullCheck.isSelected() ? null : valueField.getText();
        putButton.setText(putText(key, value));
        getButton.setText(callText("get", key));
        containsKeyButton.setText(callText("containsKey", key));
        removeButton.setText(callText("remove", key));
    }

    // =====================================================================
    // View
    // =====================================================================

    private void refreshView() {
        if (map == null) {
            keyTypeLabel.setText(stateLine("keyType", "-- (call the constructor first)"));
            keyUniverseLabel.setText(stateLine("keyUniverse.length", "-- (call the constructor first)"));
            valsLengthLabel.setText(stateLine("vals.length", "--"));
            sizeLabel.setText(stateLine("size", "--"));
            mapLabel.setText(stateLine("map", "--"));
        } else {
            // keyType is final: it is fixed by the constructor and never
            // changes for the lifetime of this map
            keyTypeLabel.setText(stateLine("keyType", getKeyType().getSimpleName() + ".class"));
            keyUniverseLabel.setText(stateLine("keyUniverse.length",
                    String.valueOf(getKeyUniverse().length)));
            valsLengthLabel.setText(stateLine("vals.length", String.valueOf(getVals().length)));
            sizeLabel.setText(stateLine("size", String.valueOf(getSize())));
            mapLabel.setText(stateLine("map", map.toString()));
        }
        valsGrid.revalidate();
        valsGrid.repaint();
    }

    /** "key              = value" with the keys aligned into one column. */
    private static String stateLine(String key, String value) {
        return String.format("%-18s = %s", key, value);
    }

    // =====================================================================
    // Vals grid: one cell per slot of the parallel "vals" array
    // =====================================================================

    /**
     * Draws one cell per slot of "vals". Cell i corresponds to the constant
     * keyUniverse[i] (ordinal i); its content is vals[i]: light gray for a
     * null slot (no mapping), blue for a real value, orange for the NULL
     * sentinel (the key is mapped to null). The most recently touched slot
     * gets an amber border.
     */
    private class ValsGrid extends JPanel {
        private static final int PAD = 8;
        private static final int GAP = 4;
        private static final int CELL_W = 66;
        private static final int CELL_H = 44;
        private static final int INDEX_H = 14;

        ValsGrid() {
            setOpaque(true);
            setBackground(Color.WHITE);
        }

        @Override
        public Dimension getPreferredSize() {
            int n = currentConstants().length;
            return new Dimension(2 * PAD + n * (CELL_W + GAP), CELL_H + INDEX_H + 2 * PAD);
        }

        @Override
        public String getToolTipText(MouseEvent e) {
            Enum<?>[] cs = currentConstants();
            for (int i = 0; i < cs.length; i++) {
                int x = PAD + i * (CELL_W + GAP);
                if (e.getX() >= x && e.getX() < x + CELL_W
                        && e.getY() >= PAD && e.getY() < PAD + CELL_H) {
                    String slot = map == null ? "--" : slotVerbose(getVals()[i]);
                    return "keyUniverse[" + i + "] = " + cs[i].name()
                            + " (ordinal " + i + ")  ->  vals[" + i + "] = " + slot;
                }
            }
            return null;
        }

        private String slotVerbose(Object slot) {
            if (slot == null) {
                return "null (no mapping)";
            }
            if (slot == nullSentinel) {
                return "NULL (sentinel: the key is mapped to null)";
            }
            return "\"" + slot + "\"";
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2 = (Graphics2D) g.create();
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);
            Enum<?>[] cs = currentConstants();
            Object[] vals = map == null ? null : getVals();

            for (int i = 0; i < cs.length; i++) {
                int x = PAD + i * (CELL_W + GAP);
                int y = PAD;

                Object slot = vals == null ? null : vals[i];
                Color fill;
                if (vals == null) {
                    fill = SLOT_EMPTY_COLOR;
                } else if (slot == null) {
                    fill = SLOT_EMPTY_COLOR;
                } else if (slot == nullSentinel) {
                    fill = SLOT_NULL_COLOR;
                } else {
                    fill = SLOT_VALUE_COLOR;
                }

                g2.setColor(fill);
                g2.fillRect(x, y, CELL_W, CELL_H);
                g2.setColor(new Color(160, 160, 160));
                g2.setStroke(new BasicStroke(1));
                g2.drawRect(x, y, CELL_W, CELL_H);
                if (i == lastTouchedIndex) {
                    g2.setColor(LAST_TOUCHED_COLOR);
                    g2.setStroke(new BasicStroke(3));
                    g2.drawRect(x + 1, y + 1, CELL_W - 2, CELL_H - 2);
                }

                boolean darkText = fill != SLOT_VALUE_COLOR;
                g2.setColor(darkText ? new Color(60, 60, 60) : Color.WHITE);
                g2.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 10));
                drawCentered(g2, cs[i].name(), x, y + 14, CELL_W);
                g2.setFont(new Font(Font.MONOSPACED, Font.BOLD, 12));
                String content = vals == null ? "?" : slot == null ? ""
                        : slot == nullSentinel ? "NULL" : String.valueOf(slot);
                drawCentered(g2, clipped(g2, content), x, y + 34, CELL_W);

                g2.setColor(new Color(120, 120, 120));
                g2.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 9));
                drawCentered(g2, "vals[" + i + "]", x, y + CELL_H + 11, CELL_W);
            }
            g2.dispose();
        }

        private void drawCentered(Graphics2D g2, String text, int x, int baseY, int width) {
            FontMetrics fm = g2.getFontMetrics();
            g2.drawString(text, x + Math.max(1, (width - fm.stringWidth(text)) / 2), baseY);
        }

        /** Truncates a value with an ellipsis when it does not fit the cell. */
        private String clipped(Graphics2D g2, String text) {
            FontMetrics fm = g2.getFontMetrics();
            if (fm.stringWidth(text) <= CELL_W - 6) {
                return text;
            }
            StringBuilder sb = new StringBuilder(text);
            while (sb.length() > 1 && fm.stringWidth(sb + "…") > CELL_W - 6) {
                sb.deleteCharAt(sb.length() - 1);
            }
            return sb + "…";
        }
    }

    // =====================================================================
    // Small helpers
    // =====================================================================

    /**
     * Startup text. Switching the enum type prints the new type as the
     * header; Reset prints the reset marker followed by the key enum line.
     */
    private void printStartupLog(boolean reset) {
        DemoEnumType t = currentEnumType();
        int n = currentConstants().length;
        logLine(reset ? "--- demo reset: call the constructor first ---"
                : "--- enum type: " + t.displayName() + " (" + t.gloss() + ") ---");
        if (reset) {
            logLine("key enum = " + t.displayName() + " (" + t.gloss() + ")");
        }
        logLine("put/get/containsKey/remove/clear are disabled until new EnumMap<>() runs");
        logLine("this demo helps you observe:");
        logLine("  new EnumMap<>(X.class) -> vals = new Object[" + n + "], all null (eager, no lazy init)");
        logLine("  put(k, v)              -> vals[k.ordinal()] = v     (ordinal IS the index: no hashing)");
        logLine("  put(k, null)           -> vals[k.ordinal()] = NULL  (maskNull: sentinel, containsKey stays true)");
        logLine("  get(k)                 -> unmaskNull(vals[k.ordinal()]): null both when absent AND when mapped to null");
        logLine("  remove(k)              -> vals[k.ordinal()] = null");
        logLine("  clear()                -> Arrays.fill(vals, null), size = 0");
        logLine("grid: gray = null slot, blue = value, orange = NULL sentinel, amber border = last touched");
    }

    /** Display form of a vals slot in the log: null / NULL / "value". */
    private String slotText(Object slot) {
        if (slot == null) {
            return "null";
        }
        if (slot == nullSentinel) {
            return "NULL";
        }
        return "\"" + slot + "\"";
    }

    /** "op(Type.KEY)": the call text shared by the buttons and the log. */
    private String callText(String op, Enum<?> key) {
        return op + "(" + currentTypeName() + "." + key.name() + ")";
    }

    /** The put(...) call text, truncated so the button stays a sane width. */
    private String putText(Enum<?> key, String value) {
        String v = value == null ? "null" : "\"" + abbreviate(value) + "\"";
        return "put(" + currentTypeName() + "." + key.name() + ", " + v + ")";
    }

    private static String abbreviate(String s) {
        return s.length() <= 6 ? s : s.substring(0, 5) + "…";
    }

    // EnumMap's type parameter is K extends Enum<K>, which a
    // Class<? extends Enum<?>> token cannot express; controlled raw calls
    // inside these helpers keep the rest of the code type-safe.
    @SuppressWarnings({"unchecked", "rawtypes"})
    private static EnumMap<?, String> newEnumMapRaw(Class<? extends Enum<?>> type) {
        return new EnumMap(type);
    }

    /** put needs the exact K, so it goes through a raw copy of the map. */
    @SuppressWarnings({"unchecked", "rawtypes"})
    private static void putRaw(EnumMap<?, String> map, Enum<?> key, String value) {
        ((EnumMap) map).put(key, value);
    }

    private DemoEnumType currentEnumType() {
        return (DemoEnumType) enumTypeCombo.getSelectedItem();
    }

    private Class<? extends Enum<?>> currentType() {
        return currentEnumType().type();
    }

    private String currentTypeName() {
        return currentType().getSimpleName();
    }

    private Enum<?>[] currentConstants() {
        return currentType().getEnumConstants();
    }

    private Enum<?> selectedConstant() {
        return (Enum<?>) keyCombo.getSelectedItem();
    }

    private Class<?> getKeyType() {
        try {
            return (Class<?>) keyTypeField.get(map);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    private Object[] getKeyUniverse() {
        try {
            return (Object[]) keyUniverseField.get(map);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    private Object[] getVals() {
        try {
            return (Object[]) valsField.get(map);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    private int getSize() {
        try {
            return sizeField.getInt(map);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    private void logLine(String line) {
        logArea.append(line + System.lineSeparator());
        logArea.setCaretPosition(logArea.getDocument().getLength());
    }

    /** Left-hand caption of a button row; fixed width keeps rows aligned. */
    private static JLabel rowCaption(String text, Color color) {
        JLabel label = new JLabel(text);
        label.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 11));
        label.setForeground(color);
        label.setPreferredSize(new Dimension(96, 18));
        return label;
    }
}

Brief class diagram for EnumMap

Ignoring generics, we can draw a brief class diagram for java.util.EnumMap:

image.png

How to generate a class diagram for java.util.EnumMap?

I used the code from the article [Java] 如何自动生成简单的 PlantUML 类图. The key commands are:

javac ClassDiagramGenerator.java
java ClassDiagramGenerator 'java.util.EnumMap'

Output:

@startuml

interface java.lang.Cloneable
interface java.io.Serializable
interface java.util.Map
abstract java.util.AbstractMap
class java.util.EnumMap
java.util.Map <|.. java.util.AbstractMap
java.util.AbstractMap <|-- java.util.EnumMap
java.io.Serializable <|.. java.util.EnumMap
java.lang.Cloneable <|.. java.util.EnumMap

@enduml

With the PlantUML plugin in IntelliJ IDEA (Community Edition), the above puml can be turned into the corresponding class diagram.

References

Comments

Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.

亚雷

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

金銀銅鐵

Okay, adjusted.