[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)

背景

大家在日常的开发过程中,应该用到过 java.util.EnumSet。在 java.util.EnumSet#noneOf 这个静态方法中,我们可以看到如下的逻辑

下方的两个类都是 EnumSet 的子类。

  • java.util.RegularEnumSet
  • java.util.JumboEnumSet

当枚举类中的元素个数 nn n 满足 n≤64n \le 64 n≤64 时,java.util.EnumSet#noneOf 方法会返回 java.util.RegularEnumSet 的一个实例。当枚举类中的元素个数 nn n 满足 n>64n \gt 64 n>64 时,java.util.EnumSet#noneOf 方法会返回 java.util.JumboEnumSet 的一个实例。

在 java.util.RegularEnumSet 中,我们可以找到一个名为 elements 的字段 ⬇️

我们可以通过反射来观察,java.util.RegularEnumSet 中增减元素时,elements 字段的变化。

正文

我借助 trae 生成了可以观察 java.util.RegularEnumSet 中 elements 字段变化的代码。为了行文连贯,我把完整的代码放到 完整代码 这一小节里了

如何运行

请将 完整代码 保存为 EnumSetBehaviorDemo.java。使用如下命令可以编译 EnumSetBehaviorDemo.java。

bash 复制代码
javac EnumSetBehaviorDemo.java

如果遇到编码的问题,可以改用下面这个命令 👇

bash 复制代码
javac -encoding UTF-8 EnumSetBehaviorDemo.java

编译之后,会得到以下 class 文件

  • EnumSetBehaviorDemo.class
  • EnumSetBehaviorDemo$BitGrid.class
  • EnumSetBehaviorDemo$DemoEnumType.class
  • EnumSetBehaviorDemo$Planet.class
  • EnumSetBehaviorDemo$地支.class
  • EnumSetBehaviorDemo$方位.class
  • EnumSetBehaviorDemo$天干.class
  • EnumSetBehaviorDemo$五行.class

使用如下命令,可以运行 EnumSetBehaviorDemo 中的 main 方法。

bash 复制代码
java --add-opens=java.base/java.util=ALL-UNNAMED EnumSetBehaviorDemo

运行效果示例

初始效果

选择一个枚举类型进行操作

一共有 5 个枚举类型可供选择 ⬇️

它们的 java 代码如下 ⬇️ (出于直观起见,和中国文化相关的五行、天干、地支、方位都是直接使用汉字,但这并不符合日常使用的 java 命名规范)

java 复制代码
/** 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 方位 { 东, 南, 西, 北 }

调用构造函数

以 五行 为例,我们可以调用 EnumSet 中的以下工厂方法

  • EnumSet.noneOf(五行.class)
  • EnumSet.allOf(五行.classs)
  • EnumSet.range(五行.金, 五行.水) (您也可以选择其他的起止元素)

添加元素

以 五行 为例,我们选择 五行 中的某个元素,然后执行对应的 add 方法 ⬇️

此时可以看到 elements 字段的变化。

add 方法定义在 java.util.Collection 接口中,我们实际调用的是 java.util.RegularEnumSet 中的 add 方法 ⬇️

删除元素

以 五行 为例,我们选择 五行 中的某个元素,然后执行对应的 remove 方法 ⬇️

此时可以看到 elements 字段的变化。

remove 方法定义在 java.util.Collection 接口中,我们实际调用的是 java.util.RegularEnumSet 中的 remove 方法 ⬇️

clear() 按钮

以 五行 为例,我们点击 clear() 按钮,就可以清空 EnumSet。

此时可以看到 elements 字段的变化。

clear() 方法定义在 java.util. Set 接口中,我们实际调用的是 java.util.RegularEnumSet 中的 clear() 方法 ⬇️

Reset 按钮

这个按钮用于重新开始。

完整代码

java 复制代码
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.lang.reflect.Field;
import java.util.EnumSet;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.border.TitledBorder;

/**
 * Swing demo inspired by ArrayListBehaviorDemo: it visualizes the internal
 * state of java.util.RegularEnumSet while operations are executed.
 *
 * RegularEnumSet stores its members in a single private long field
 * "elements", used as a bitmask: bit i is set iff the enum constant with
 * ordinal i is a member of the set. This demo reads that field via
 * reflection and draws one cell per enum constant.
 *
 * Observable facts (each demo enum has <= 64 constants, so the set is always
 * a RegularEnumSet; bit i of "elements" corresponds to ordinal i):
 * 1) EnumSet.noneOf(Planet.class) -> elements = 0 right away (allocated
 *    eagerly, no lazy init like ArrayList's empty-arg constructor);
 * 2) EnumSet.allOf(Planet.class)  -> elements = (1L << 8) - 1 = 255;
 * 3) add(e)    -> elements |= 1L << e.ordinal()     (bitwise OR);
 * 4) remove(e) -> elements &= ~(1L << e.ordinal())  (bitwise AND NOT);
 * 5) clear()   -> elements = 0;
 * 6) size == Long.bitCount(elements).
 *
 * The "elements" field is private, so reflection is required. On JDK 9+ run with:
 * javac EnumSetBehaviorDemo.java
 * java --add-opens=java.base/java.util=ALL-UNNAMED EnumSetBehaviorDemo
 */
public class EnumSetBehaviorDemo {

    // ---- selectable enum universes (each <= 64 constants => RegularEnumSet) ----

    /** 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 ArrayListBehaviorDemo ----
    /** 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);

    private static final Color BIT_SET_COLOR = new Color(66, 133, 244);
    private static final Color BIT_CLEAR_COLOR = new Color(240, 240, 240);
    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);

    // ---- state ----
    /** null means "no set yet": operations are disabled until a factory method runs. */
    private EnumSet<?> set;
    private int lastTouchedBit = -1;

    // ---- UI ----
    private JButton noneOfButton;
    private JButton allOfButton;
    private JButton addButton;
    private JButton removeButton;
    private JButton clearButton;
    private JButton resetButton;
    private JButton rangeButton;
    private JComboBox<Object> elementCombo;
    private JComboBox<Object> rangeFromCombo;
    private JComboBox<Object> rangeToCombo;
    private JComboBox<DemoEnumType> enumTypeCombo;
    private JLabel glossLabel;
    private JTextArea sourceArea;

    private JLabel runtimeClassLabel;
    private JLabel elementsLabel;
    private JLabel bitCountLabel;
    private JLabel membershipLabel;
    private JPanel statePanel;
    private JLabel binaryLabel;
    private JTextArea logArea;
    private BitGrid bitGrid;

    public static void main(String[] args) {
        SwingUtilities.invokeLater(EnumSetBehaviorDemo::new);
    }

    private EnumSetBehaviorDemo() {
        createAndShowGui();
    }

    private void createAndShowGui() {
        // Snappier, longer-lived tooltips: the defaults (750 ms to appear,
        // 4 s auto-dismiss) make a mapping table this long feel unreliable.
        ToolTipManager.sharedInstance().setInitialDelay(300);
        ToolTipManager.sharedInstance().setDismissDelay(12000);

        JFrame frame = new JFrame("RegularEnumSet 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("enum type ="));
        typeGroup.add(enumTypeCombo);
        typeGroup.add(glossLabel);

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

        // ---------- row 1: static factory methods ----------
        JPanel ctorRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
        ctorRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        noneOfButton = new JButton();
        allOfButton = new JButton();
        noneOfButton.setFont(CODE_FONT);
        allOfButton.setFont(CODE_FONT);
        ctorRow.add(rowCaption("factory method", CODE_COLOR));
        ctorRow.add(noneOfButton);
        ctorRow.add(allOfButton);

        // EnumSet.range(from, to) is also a static factory: it builds a
        // brand-new set from a contiguous run of constants, so it lives on
        // the factory-method row, with its two arguments grouped beside it.
        rangeFromCombo = new JComboBox<>();
        for (Enum<?> c : currentConstants()) {
            rangeFromCombo.addItem(c);
        }
        rangeToCombo = new JComboBox<>();
        for (Enum<?> c : currentConstants()) {
            rangeToCombo.addItem(c);
        }
        rangeButton = new JButton();
        rangeButton.setFont(CODE_FONT);
        updateRangeButtonText();
        rangeFromCombo.addActionListener(e -> updateRangeButtonText());
        rangeToCombo.addActionListener(e -> updateRangeButtonText());

        JPanel rangeGroup = new JPanel();
        rangeGroup.setBorder(BorderFactory.createEtchedBorder());
        rangeGroup.add(new JLabel("from"));
        rangeGroup.add(rangeFromCombo);
        rangeGroup.add(new JLabel("to"));
        rangeGroup.add(rangeToCombo);
        rangeGroup.add(rangeButton);
        control.add(ctorRow);

        // Dedicated second factory-method row: with long enum names the range
        // button text no longer fits next to noneOf/allOf in one FlowLayout
        // row (FlowLayout clips instead of wrapping), cutting the group off
        // at the window edge.
        JPanel rangeRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
        rangeRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        rangeRow.add(rowCaption("factory method", CODE_COLOR));
        rangeRow.add(rangeGroup);
        control.add(rangeRow);

        // ---------- row 2: Java API calls ----------
        JPanel opRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
        opRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        addButton = new JButton("add(...)");
        removeButton = new JButton("remove(...)");
        clearButton = new JButton("clear()");
        elementCombo = new JComboBox<>();
        for (Enum<?> c : currentConstants()) {
            elementCombo.addItem(c);
        }

        // The button text is dynamic: it shows the exact call that will run
        // for the constant currently selected in the combo box.
        updateOpButtonText();
        elementCombo.addActionListener(e -> updateOpButtonText());

        addButton.setFont(CODE_FONT);
        removeButton.setFont(CODE_FONT);
        clearButton.setFont(CODE_FONT);
        addButton.setToolTipText("Runs: set.add(e) where e is the selected constant");
        removeButton.setToolTipText("Runs: set.remove(e) where e is the selected constant");
        clearButton.setToolTipText("Runs: set.clear()");

        // Group the element selector with the operations that consume it
        JPanel selGroup = new JPanel();
        selGroup.setBorder(BorderFactory.createEtchedBorder());
        selGroup.add(new JLabel("element ="));
        selGroup.add(elementCombo);

        opRow.add(rowCaption("java API", CODE_COLOR));
        opRow.add(selGroup);
        opRow.add(addButton);
        opRow.add(removeButton);
        opRow.add(clearButton);
        control.add(opRow);

        // ---------- row 3: 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 ----------
        // Every line follows the same "key = value" pattern (monospace, keys
        // padded so the '=' column aligns); the binary line carries its own
        // "elements = 0b..." label instead of showing bare bits.
        statePanel = new JPanel();
        statePanel.setLayout(new BoxLayout(statePanel, BoxLayout.Y_AXIS));
        statePanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        statePanel.setBorder(new TitledBorder("internal state of the EnumSet"));

        runtimeClassLabel = new JLabel();
        elementsLabel = new JLabel();
        binaryLabel = new JLabel();
        bitCountLabel = new JLabel();
        membershipLabel = new JLabel();
        for (JLabel line : new JLabel[]{runtimeClassLabel, elementsLabel, binaryLabel,
                bitCountLabel, membershipLabel}) {
            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);
        }

        bitGrid = new BitGrid();
        statePanel.add(bitGrid);

        // ---------- source code panel ----------
        // Shows the declaration of the selected enum, generated from its
        // constants so it can never disagree with the runtime data.
        sourceArea = new JTextArea();
        sourceArea.setEditable(false);
        sourceArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
        JScrollPane sourceScroll = new JScrollPane(sourceArea);
        sourceScroll.setBorder(new TitledBorder("Source code of the demo enum"));

        // Side by side: the bitmask state on the left, the enum source
        // (which defines the bit order) on the right. A split pane instead
        // of BorderLayout: with WEST + CENTER, the source panel silently
        // absorbed every width deficit and could collapse to a sliver
        // whenever the window got narrower than the fixed-width state
        // panel. resizeWeight keeps both sides usable while resizing.
        JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, statePanel, sourceScroll);
        split.setBorder(null);
        split.setContinuousLayout(true);
        split.setOneTouchExpandable(true);
        split.setResizeWeight(0.70);

        // ---------- 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, 170));

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

        // ---------- listeners ----------
        enumTypeCombo.addActionListener(e -> switchEnumType());
        noneOfButton.addActionListener(e -> doNoneOf());
        allOfButton.addActionListener(e -> doAllOf());
        rangeButton.addActionListener(e -> doRange());
        addButton.addActionListener(e -> doAdd());
        removeButton.addActionListener(e -> doRemove());
        clearButton.addActionListener(e -> doClear());
        resetButton.addActionListener(e -> resetDemo());

        switchEnumType();

        frame.setSize(1200, 760);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
        // proportional divider position needs the final component size
        split.setDividerLocation(0.68);
    }

    // =====================================================================
    // Operations (each logs before/after of the "elements" bitmask)
    // =====================================================================

    private void doNoneOf() {
        set = noneOfRaw(currentType());
        lastTouchedBit = -1;
        setSetCreatedState();
        logLine(String.format("%-32s elements %-27s [%s]",
                "EnumSet.noneOf(" + currentTypeName() + ".class)", fmtBits(getElements()),
                "elements = 0 (allocated eagerly: no lazy init like ArrayList)"));
        refreshView();
    }

    private void doAllOf() {
        set = allOfRaw(currentType());
        lastTouchedBit = -1;
        setSetCreatedState();
        int n = currentConstants().length;
        logLine(String.format("%-32s elements %-27s [%s]",
                "EnumSet.allOf(" + currentTypeName() + ".class)", fmtBits(getElements()),
                "elements = (1L << " + n + ") - 1 = " + ((1L << n) - 1) + " (every bit set)"));
        refreshView();
    }

    private void doRange() {
        Enum<?> from = (Enum<?>) rangeFromCombo.getSelectedItem();
        Enum<?> to = (Enum<?>) rangeToCombo.getSelectedItem();
        String opText = "EnumSet.range(" + currentTypeName() + "." + from.name()
                + ", " + currentTypeName() + "." + to.name() + ")";
        try {
            set = rangeRaw(from, to);
        } catch (IllegalArgumentException ex) {
            // EnumSet.range requires from.ordinal() <= to.ordinal()
            logLine(String.format("%-32s threw %s", opText, ex));
            return;
        }
        lastTouchedBit = -1;
        setSetCreatedState();
        logLine(String.format("%-32s elements %-27s [%s]",
                opText, fmtBits(getElements()),
                "(1L << " + (to.ordinal() + 1) + ") - (1L << " + from.ordinal()
                        + ") = " + getElements() + " (contiguous bits "
                        + from.ordinal() + ".." + to.ordinal() + " set)"));
        refreshView();
    }

    private void doAdd() {
        Enum<?> sel = selectedConstant();
        long old = getElements();
        boolean changed = addToSet(sel);
        long now = getElements();
        // highlight only a bit that actually changed; a no-op add must not
        // mark the cell orange, since elements is untouched
        lastTouchedBit = changed ? sel.ordinal() : -1;

        String note = changed
                ? "elements |= 1L << " + sel.ordinal() + "  (bit " + sel.ordinal() + " set by OR)"
                : "no-op: bit " + sel.ordinal() + " was already set";
        logLine(String.format("%-32s elements %-27s [%s]",
                "add(" + currentTypeName() + "." + sel.name() + ")", changeText(old, now), note));
        refreshView();
    }

    private void doRemove() {
        Enum<?> sel = selectedConstant();
        long old = getElements();
        boolean changed = set.remove(sel);
        long now = getElements();
        lastTouchedBit = changed ? sel.ordinal() : -1;

        String note = changed
                ? "elements &= ~(1L << " + sel.ordinal() + ")  (bit " + sel.ordinal() + " cleared by AND NOT)"
                : "no-op: bit " + sel.ordinal() + " was not set";
        logLine(String.format("%-32s elements %-27s [%s]",
                "remove(" + currentTypeName() + "." + sel.name() + ")", changeText(old, now), note));
        refreshView();
    }

    private void doClear() {
        long old = getElements();
        set.clear();
        long now = getElements();
        lastTouchedBit = -1;
        logLine(String.format("%-32s elements %-27s [%s]",
                "clear()", changeText(old, now), "all bits cleared"));
        refreshView();
    }

    private void resetDemo() {
        set = null;
        lastTouchedBit = -1;
        setNoSetState();
        logArea.setText("");
        logLine("--- demo reset: call a factory method first ---");
        printStartupLog();
        refreshView();
    }

    /**
     * Rebuilds everything that depends on the selected enum type. Called at
     * startup and whenever the user picks another entry in the combo box;
     * switching discards the current set (a new type means a new demo).
     */
    private void switchEnumType() {
        set = null;
        lastTouchedBit = -1;
        DemoEnumType t = currentEnumType();

        elementCombo.removeAllItems();
        for (Enum<?> c : currentConstants()) {
            elementCombo.addItem(c);
        }
        rangeFromCombo.removeAllItems();
        rangeToCombo.removeAllItems();
        for (Enum<?> c : currentConstants()) {
            rangeFromCombo.addItem(c);
            rangeToCombo.addItem(c);
        }
        updateRangeButtonText();
        glossLabel.setText(t.gloss() + ", " + currentConstants().length + " constants");
        noneOfButton.setText("EnumSet.noneOf(" + currentTypeName() + ".class)");
        allOfButton.setText("EnumSet.allOf(" + currentTypeName() + ".class)");
        noneOfButton.setToolTipText("Runs: EnumSet.noneOf(" + currentTypeName() + ".class)");
        allOfButton.setToolTipText("Runs: EnumSet.allOf(" + currentTypeName() + ".class)");
        sourceArea.setText(buildEnumSource(currentType()));
        applyElementsTooltip();

        setNoSetState();
        logArea.setText("");
        logLine("--- enum type: " + t.displayName() + " (" + t.gloss() + ") ---");
        printStartupLog();
        refreshView();
    }

    private void printStartupLog() {
        logLine("  add(...), remove(...) and clear() are disabled until a factory method is called");
        logLine("what to observe:");
        logLine("  EnumSet.noneOf(<T>.class) -> elements = 0 immediately (no lazy init like ArrayList)");
        logLine("  EnumSet.allOf(<T>.class)  -> elements = (1L << n) - 1, n = number of constants");
        logLine("  add(e)                    -> elements |= 1L << e.ordinal()");
        logLine("  remove(e)                 -> elements &= ~(1L << e.ordinal())");
        logLine("  clear()                   -> elements = 0");
        logLine("  bit i of elements is 1 iff the constant with ordinal i is a member");
        logLine("");
    }

    // =====================================================================
    // Button state machine (same pattern as ArrayListBehaviorDemo)
    // =====================================================================

    private void setNoSetState() {
        noneOfButton.setEnabled(true);
        allOfButton.setEnabled(true);
        rangeFromCombo.setEnabled(true);
        rangeToCombo.setEnabled(true);
        rangeButton.setEnabled(true);
        elementCombo.setEnabled(false);
        addButton.setEnabled(false);
        removeButton.setEnabled(false);
        clearButton.setEnabled(false);
        resetButton.setEnabled(false);
        refreshButtonColors();
    }

    private void setSetCreatedState() {
        noneOfButton.setEnabled(false);
        allOfButton.setEnabled(false);
        rangeFromCombo.setEnabled(false);
        rangeToCombo.setEnabled(false);
        rangeButton.setEnabled(false);
        elementCombo.setEnabled(true);
        addButton.setEnabled(true);
        removeButton.setEnabled(true);
        clearButton.setEnabled(true);
        resetButton.setEnabled(true);
        refreshButtonColors();
    }

    /**
     * Keeps text colors meaningful: Java-mapped buttons are blue, demo
     * helpers dark gray, and anything disabled turns light gray so a
     * disabled button can never be mistaken for a clickable one.
     */
    private void refreshButtonColors() {
        noneOfButton.setForeground(noneOfButton.isEnabled() ? CODE_COLOR : DISABLED_COLOR);
        allOfButton.setForeground(allOfButton.isEnabled() ? CODE_COLOR : DISABLED_COLOR);
        rangeButton.setForeground(rangeButton.isEnabled() ? CODE_COLOR : DISABLED_COLOR);
        addButton.setForeground(addButton.isEnabled() ? CODE_COLOR : DISABLED_COLOR);
        removeButton.setForeground(removeButton.isEnabled() ? CODE_COLOR : DISABLED_COLOR);
        clearButton.setForeground(clearButton.isEnabled() ? CODE_COLOR : DISABLED_COLOR);
        resetButton.setForeground(resetButton.isEnabled() ? DEMO_COLOR : DISABLED_COLOR);
    }

    /**
     * Button labels for add/remove always show the currently selected constant.
     */
    private void updateOpButtonText() {
        Enum<?> sel = selectedConstant();
        if (sel != null && addButton != null && removeButton != null) {
            // qualified form: reads exactly like the code the user would write
            addButton.setText("add(" + currentTypeName() + "." + sel.name() + ")");
            removeButton.setText("remove(" + currentTypeName() + "." + sel.name() + ")");
        }
    }

    /** Range button text mirrors the exact factory call with its two args. */
    private void updateRangeButtonText() {
        if (rangeButton == null) {
            return;
        }
        Object from = rangeFromCombo.getSelectedItem();
        Object to = rangeToCombo.getSelectedItem();
        if (from instanceof Enum<?> f && to instanceof Enum<?> t) {
            String text = "EnumSet.range(" + currentTypeName() + "." + f.name()
                    + ", " + currentTypeName() + "." + t.name() + ")";
            rangeButton.setText(text);
            rangeButton.setToolTipText("Runs: " + text
                    + "  (throws IllegalArgumentException when from.ordinal() > to.ordinal())");
        }
    }

    // =====================================================================
    // Reflection access + type-token helpers for the selected enum
    // =====================================================================

    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<?>) elementCombo.getSelectedItem();
    }

    // EnumSet.noneOf/allOf need the self-referencing type E extends Enum<E>,
    // which a Class<? extends Enum<?>> token cannot express; a controlled
    // raw call inside these helpers keeps the rest of the code type-safe.
    @SuppressWarnings({"unchecked", "rawtypes"})
    private static EnumSet<?> noneOfRaw(Class<? extends Enum<?>> type) {
        return EnumSet.noneOf((Class) type);
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private static EnumSet<?> allOfRaw(Class<? extends Enum<?>> type) {
        return EnumSet.allOf((Class) type);
    }

    /** Same for add: the raw set accepts any enum constant it was built for. */
    @SuppressWarnings({"unchecked", "rawtypes"})
    private boolean addToSet(Enum<?> e) {
        return ((EnumSet) set).add(e);
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private static EnumSet<?> rangeRaw(Enum<?> from, Enum<?> to) {
        return EnumSet.range((Enum) from, (Enum) to);
    }

    private long getElements() {
        try {
            Field f = set.getClass().getDeclaredField("elements");
            f.setAccessible(true);
            return f.getLong(set);
        } catch (ReflectiveOperationException e) {
            throw new IllegalStateException("cannot read RegularEnumSet.elements", e);
        }
    }

    // =====================================================================
    // View refresh
    // =====================================================================

    private void refreshView() {
        if (set == null) {
            runtimeClassLabel.setText(stateLine("runtime class", "--"));
            elementsLabel.setText(stateLine("elements", "-- (call a factory method first)"));
            binaryLabel.setText(stateLine("elements", "--"));
            bitCountLabel.setText(stateLine("size", "--"));
            membershipLabel.setText(stateLine("set", "--"));
        } else {
            long e = getElements();
            runtimeClassLabel.setText(stateLine("runtime class", set.getClass().getName()));
            elementsLabel.setText(stateLine("elements",
                    e + " (0x" + Long.toHexString(e).toUpperCase() + ")"));
            binaryLabel.setText(htmlStateLine("elements", toGroupedBinaryHtml(e)));
            bitCountLabel.setText(stateLine("size",
                    "Long.bitCount(elements) = " + Long.bitCount(e)));
            membershipLabel.setText(stateLine("set", String.valueOf(set)));
        }
        bitGrid.repaint();
    }

    /** Uniform "key = value" layout; keys padded so the '=' column aligns. */
    private static String stateLine(String key, String value) {
        return String.format("%-14s= %s", key, value);
    }

    /**
     * Hover help for the state panel: the rule connecting bits to membership
     * plus the per-constant mapping for the currently selected enum.
     */
    private String buildElementsTooltip() {
        StringBuilder sb = new StringBuilder("<html><b>bit i of elements is 1 "
                + "iff the constant with ordinal i is a member</b>");
        sb.append("<br><font color='").append(colorHex(BIT_SET_COLOR))
          .append("'>blue</font> = this enum's bits 0..")
          .append(currentConstants().length - 1)
          .append(", <font color='").append(colorHex(LAST_TOUCHED_COLOR))
          .append("'>orange</font> = last changed bit, gray = not used by this enum");
        Enum<?>[] values = currentConstants();
        for (int i = 0; i < values.length; i++) {
            sb.append("<br>bit ").append(i).append(" \u2194 ")
              .append(currentTypeName()).append('.').append(values[i].name());
        }
        sb.append("</html>");
        return sb.toString();
    }

    /**
     * Swing picks the tooltip of the DEEPEST component under the cursor, so
     * attaching it only to the panel would leave the row containers and gaps
     * with no help at all. Walk the subtree and attach the same text to every
     * child, making the hover target effectively the whole panel.
     */
    private void applyElementsTooltip() {
        setTooltipRecursive(statePanel, buildElementsTooltip());
    }

    private static void setTooltipRecursive(JComponent c, String tip) {
        c.setToolTipText(tip);
        for (Component child : c.getComponents()) {
            if (child instanceof JComponent) {
                setTooltipRecursive((JComponent) child, tip);
            }
        }
    }

    /**
     * The Java source of the selected enum, generated from its constants so
     * names and ordinals can never disagree with the runtime data.
     */
    private static String buildEnumSource(Class<? extends Enum<?>> type) {
        Enum<?>[] values = type.getEnumConstants();
        StringBuilder sb = new StringBuilder("enum ").append(type.getSimpleName()).append(" {\n");
        for (int i = 0; i < values.length; i++) {
            String token = values[i].name() + (i < values.length - 1 ? "," : "");
            sb.append("    ").append(String.format("%-9s", token))
              .append("// ordinal ").append(i).append('\n');
        }
        sb.append('}');
        return sb.toString();
    }

    /** HTML variant of {@link #stateLine}: &nbsp; padding survives Swing's
     * HTML whitespace collapsing, so the '=' column still aligns. */
    private static String htmlStateLine(String key, String valueHtml) {
        String pad = "&nbsp;".repeat(Math.max(1, 14 - key.length()));
        return "<html>" + key + pad + "= " + valueHtml + "</html>";
    }

    /**
     * 64 bits grouped by 8, color-coded to match the bit grid: blue marks
     * the bits this enum actually uses (0..n-1), gray the never-used high
     * bits, orange-bold the bit changed by the most recent operation.
     */
    private String toGroupedBinaryHtml(long v) {
        String setHex = colorHex(BIT_SET_COLOR);
        String touchHex = colorHex(LAST_TOUCHED_COLOR);
        String raw = String.format("%64s", Long.toBinaryString(v)).replace(' ', '0');
        int n = currentConstants().length;
        StringBuilder sb = new StringBuilder("0b");
        for (int i = 0; i < 64; i++) {
            if (i > 0 && i % 8 == 0) {
                sb.append('_');
            }
            int bit = 63 - i;
            char c = raw.charAt(i);
            if (set != null && bit == lastTouchedBit) {
                sb.append("<font color='").append(touchHex).append("'><b>")
                  .append(c).append("</b></font>");
            } else if (bit < n) {
                sb.append("<font color='").append(setHex).append("'>")
                  .append(c).append("</font>");
            } else {
                sb.append("<font color='#bdbdbd'>").append(c).append("</font>");
            }
        }
        return sb.toString();
    }

    private static String colorHex(Color c) {
        return String.format("#%06x", c.getRGB() & 0xFFFFFF);
    }

    private static String fmtBits(long v) {
        return v + " (0x" + Long.toHexString(v).toUpperCase() + ")";
    }

    /** "a -> b" only when the value actually changed. */
    private static String changeText(long a, long b) {
        return a == b ? fmtBits(a) : fmtBits(a) + " -> " + fmtBits(b);
    }

    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;
    }

    // =====================================================================
    // Bit grid: one cell per enum constant, filled when its bit is set
    // =====================================================================

    /** Draws one cell per enum constant: blue when its bit is set. */
    private final class BitGrid extends JPanel {
        private static final int MAX_CELL_W = 110;
        private static final int CELL_H = 84;
        private static final int GAP = 6;
        /** Fixed panel width keeps the state panel stable across enum types. */
        private static final int GRID_W = 830;

        BitGrid() {
            setPreferredSize(new Dimension(GRID_W, CELL_H + 2 * GAP));
            setBackground(Color.WHITE);
        }

        @Override
        protected void paintComponent(Graphics g0) {
            super.paintComponent(g0);
            Graphics2D g = (Graphics2D) g0;
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

            Enum<?>[] values = currentConstants();
            if (values == null || values.length == 0) {
                return;
            }
            int n = values.length;
            // shrink cells for larger enums (e.g. 12 earthly branches), keep
            // the row of cells centered in the panel
            int cellW = Math.min(MAX_CELL_W, (getWidth() - (n + 1) * GAP) / n);
            int totalW = n * cellW + (n - 1) * GAP;
            int startX = (getWidth() - totalW) / 2;
            boolean compact = cellW < 72;

            long elements = (set == null) ? 0L : getElements();
            for (int i = 0; i < n; i++) {
                int x = startX + i * (cellW + GAP);
                boolean bitSet = set != null && ((elements >> i) & 1L) == 1L;

                g.setColor(bitSet ? BIT_SET_COLOR : BIT_CLEAR_COLOR);
                g.fillRect(x, GAP, cellW, CELL_H);

                if (set != null && lastTouchedBit == i) {
                    g.setColor(LAST_TOUCHED_COLOR);
                    g.setStroke(new BasicStroke(3));
                } else {
                    g.setColor(new Color(180, 180, 180));
                    g.setStroke(new BasicStroke(1));
                }
                g.drawRect(x, GAP, cellW, CELL_H);

                // constant name
                g.setColor(bitSet ? Color.WHITE : Color.DARK_GRAY);
                g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, compact ? 10 : 11));
                FontMetrics fm = g.getFontMetrics();
                String name = values[i].name();
                g.drawString(name, x + (cellW - fm.stringWidth(name)) / 2, GAP + 18);

                // bit position
                g.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, compact ? 9 : 10));
                fm = g.getFontMetrics();
                String ord = "bit " + i;
                g.drawString(ord, x + (cellW - fm.stringWidth(ord)) / 2, GAP + 32);

                // bit value
                g.setFont(new Font(Font.MONOSPACED, Font.BOLD, compact ? 16 : 20));
                fm = g.getFontMetrics();
                String bit = set == null ? "?" : bitSet ? "1" : "0";
                g.drawString(bit, x + (cellW - fm.stringWidth(bit)) / 2, GAP + 66);
            }
        }
    }
}

参考资料

相关推荐
福建佰胜张工1 小时前
SMC 薄型气爪 MHF2-16D1 深度解析:平行开闭、低剖面结构在自动化装配中的选型与落地
java·服务器·安全·自动化
念何架构之路1 小时前
go-grpc设计思想
开发语言·后端·golang
catoop1 小时前
API Key 设计与安全生成方案:结构拆解、数据库设计与 Java 实现
java·数据库·安全
Flynt2 小时前
GitHub 17万星的"懒人规则",我花2毛钱让 AI 带/不带它各写了一遍
agent·ai编程·claude
system_error_x2 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
驭渊的小故事2 小时前
Spring AI Alibaba 实战:从maven依赖治理到声明式工具调用
java·人工智能·spring
Elastic 中国社区官方博客3 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
ServBay3 小时前
AI说代码没漏洞?马上被 PHP 打脸了
aigc·php·ai编程
tyn1883 小时前
记录本地安装nacos服务的方法及异常
java·开发语言