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

217 阅读18分钟

背景

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

image.png

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

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

java.util.EnumSet#noneOf 方法所返回的对象的精确类型可以分为以下两种情况 ⬇️

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

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

image.png

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

注意事项

完整代码 里用到了 record 语法,需要使用 JDK 16 及以上(写作本文时,我使用的是 JDK 21)。

image.png

正文

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

如何运行

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

javac RegularEnumSetBehaviorDemo.java

完整代码 中有 五行、天干 等中文标识符,如果您遇到编码问题,可以试试下面这个命令 👇

javac -encoding UTF-8 RegularEnumSetBehaviorDemo.java

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

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

使用如下命令,可以运行 RegularEnumSetBehaviorDemo 中的 main 方法。(通过反射读取 RegularEnumSet 的私有 elements 字段,受 JDK 9+ 模块系统限制,需要使用 --add-opens 选项)

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

运行效果示例

初始效果

image.png

internal state of EnumSet 面板

界面左侧的 internal state of the EnumSet 面板会展示 elements 字段的当前值,每次操作后都会刷新。以 EnumSet.noneOf(五行.class) 为例,该面板会显示 5 行信息 ⬇️

  • runtime class:set 的运行时类型,即 java.util.RegularEnumSet。这一行可以直接印证前面 背景 中的结论——noneOf 返回的确实是 RegularEnumSet 的实例
  • elements(十进制):elements 的十进制值,括号内为其十六进制形式
  • elements(二进制):elements 的二进制形式,按位分组显示
  • size:set 的大小,即 Long.bitCount(elements)。RegularEnumSet 并没有单独保存元素个数,size() 是直接数 elements 中 1 的个数得到的
  • set:EnumSet 中当前包含的枚举常量

位与 ordinal 的对应关系

elements 是一个位掩码(bitmask):第 i 位为 1,当且仅当 ordinal 为 i 的枚举常量属于该集合。以 五行 为例,金 的 ordinal 是 0,所以它对应最低位(bit 0),土 的 ordinal 是 4,对应 bit 4。

面板下方的方格图正是按这个规则画的:从右到左 依次是 bit 0、bit 1 ……,每个方格中写着该常量对应的 bit i 和它当前的取值(1 表示属于集合,0 表示不属于)。尚未调用工厂方法时集合还不存在,方格里的取值显示为 ?。

因此 EnumSet.allOf(五行.class) 得到的 elements 是 31(0b11111,低 5 位全为 1),而 EnumSet.range(五行.金, 五行.水) 得到的 elements 是 7(0b111,bit 0 到 bit 2 为 1)。

另外,最近一次真正改变了状态的那一位,在方格图中会以橙色边框标出;如果操作没有改变任何位(例如重复 add 已存在的元素),则不会出现这个高亮。

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

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

  • Planet
  • 五行
  • 天干
  • 地支
  • 方位

image.png

它们的 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(五行.class)
  • EnumSet.range(五行.金, 五行.水)(您也可以选择其他的起止元素)

下图中用红框标出了三个静态工厂方法对应的按钮 ⬇️

image.png

range 参数非法时

EnumSet.range(from, to) 要求 from.ordinal() <= to.ordinal(),否则抛出 IllegalArgumentException。

例如执行 EnumSet.range(五行.土, 五行.金) 时,界面会有三处反馈 ⬇️

  • 在 Operation log 中追加一行,记录抛出的异常
  • 在界面上方显示红色横幅,写明 from 与 to 的 ordinal
  • 用红色边框标出 from/to 所在的控件组

image.png

⬆️ 图中红色箭头分别指向触发报错的 range 按钮、上方的红色横幅,以及 Operation log 中的异常记录。

此后只要成功执行一次操作,或切换枚举类型、点击 Reset 按钮,报错状态都会清除。

添加元素

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

image.png

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

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

image.png

删除元素

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

image.png

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

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

image.png

clear() 按钮

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

image.png

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

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

image.png

Operation log 面板

界面底部的 Operation log 面板会记录每一次操作。启动时的日志如下 ⬇️

image.png

第一行标明当前选择的枚举类型;第二行提醒您:在调用工厂方法之前,add(...)、remove(...) 和 clear() 都是禁用的(按钮呈灰色)。从第三行开始,它会列出这个工具可以辅助您观察的几条规律:

  • noneOf 使 elements 等于 0
  • allOf 使 elements 等于 (1L << n) - 1
  • add 会把目标位置为 1,即 elements |= (1L << ordinal)
  • remove 会把目标位清零、其余位保持不变,即 elements &= ~(1L << ordinal)。这里的 ~ 作用在 1L << ordinal 这个掩码上(把"只有目标位为 1"变成"只有目标位为 0"),再与原值按位与
  • clear() 使 elements 变为 0
  • 第 i 位为 1 当且仅当 ordinal 为 i 的常量是成员。

此后每执行一次操作,日志会追加一行,并自动滚动到最新一行。每一行分为三列 ⬇️

  1. 该次调用的表达式,例如 add(五行.木)
  2. elements 的变化,写法为"操作前(十进制,括号内为十六进制)" -> "操作后";如果这次操作没有改变任何位,则只显示一个值,不会出现 ->
  3. 中括号内的一句说明,解释这次操作对位做了什么,或者说明这是一次 no-op

以"添加元素"那一小节的截图为参考,执行 EnumSet.noneOf(五行.class) 后再执行 add(五行.木),日志新增的一行如下:

add(五行.木)                        elements 0 (0x0) -> 2 (0x2)          [elements |= 1L << 1  (bit 1 set by OR)]
  • 第 1 列 add(五行.木):调用表达式
  • 第 2 列 elements 0 (0x0) -> 2 (0x2):从 0 变为 2(二进制 0b10),也就是将 bit 1 置位
  • 第 3 列 [elements |= 1L << 1 (bit 1 set by OR)]:这次操作对应的位运算

Reset 按钮

这个按钮用于重新开始。

image.png

完整代码

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.
 * <p>
 * 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.
 * <p>
 * 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).
 * <p>
 * The "elements" field is private, so reflection is required. On JDK 9+ run with:
 * javac RegularEnumSetBehaviorDemo.java
 * java --add-opens=java.base/java.util=ALL-UNNAMED RegularEnumSetBehaviorDemo
 */
public class RegularEnumSetBehaviorDemo {

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

    /** Error feedback: light red banner background, dark red text/border. */
    private static final Color ERROR_BG = new Color(253, 237, 237);
    private static final Color ERROR_FG = new Color(183, 28, 28);
    private static final Color ERROR_LINE = new Color(239, 83, 80);

    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 JPanel rangeGroup;
    private JLabel errorBanner;

    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(RegularEnumSetBehaviorDemo::new);
    }

    private RegularEnumSetBehaviorDemo() {
        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());

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

        // ---------- error banner: hidden until an API call fails ----------
        errorBanner = new JLabel();
        errorBanner.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
        errorBanner.setForeground(ERROR_FG);
        errorBanner.setOpaque(true);
        errorBanner.setBackground(ERROR_BG);
        errorBanner.setBorder(BorderFactory.createCompoundBorder(
                BorderFactory.createLineBorder(ERROR_LINE),
                BorderFactory.createEmptyBorder(4, 8, 4, 8)));
        errorBanner.setVisible(false);
        JPanel bannerRow = new JPanel(new BorderLayout());
        bannerRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        bannerRow.add(errorBanner, BorderLayout.CENTER);
        control.add(bannerRow);

        // ---------- 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() {
        clearError();
        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() {
        clearError();
        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));
            showError(opText + " failed: IllegalArgumentException: from.ordinal() ("
                    + from.ordinal() + ") must be <= to.ordinal() (" + to.ordinal() + ")");
            return;
        }
        clearError();
        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() {
        clearError();
        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() {
        clearError();
        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() {
        clearError();
        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();
        clearError();
        logArea.setText("");
        logLine("--- demo reset: call a factory method first ---");
        printStartupLog();
        refreshView();
    }

    /**
     * Error feedback has three channels: the log line (written by the
     * caller), this banner, and a red border around the control group whose
     * arguments caused the failure. The next successful operation clears
     * the error state automatically (see the clearError calls).
     */
    private void showError(String message) {
        errorBanner.setText(message);
        errorBanner.setVisible(true);
        rangeGroup.setBorder(BorderFactory.createCompoundBorder(
                BorderFactory.createLineBorder(ERROR_LINE, 2),
                BorderFactory.createEmptyBorder(2, 2, 2, 2)));
    }

    private void clearError() {
        errorBanner.setVisible(false);
        rangeGroup.setBorder(BorderFactory.createEtchedBorder());
    }

    /**
     * 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;
        clearError();
        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(" ↔ ")
              .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. Bits run
     * MSB-first (bit n-1 on the left ... bit 0 on the right), matching the
     * binary text in the state panel above the grid.
     */
    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 p = 0; p < n; p++) {
                // display column p shows ordinal (n-1-p): MSB on the left,
                // matching the binary text in the state panel
                int i = n - 1 - p;
                int x = startX + p * (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);
            }
        }
    }
}


RegularEnumSet 的简要类图

如果忽略泛型,我们可以为 java.util.RegularEnumSet 画出简要类图 ⬇️

image.png

如何为 java.util.RegularEnumSet 绘制类图?

我使用了 [Java] 如何自动生成简单的 PlantUML 类图 一文中提供的代码。关键的命令如下

javac ClassDiagramGenerator.java
java ClassDiagramGenerator 'java.util.RegularEnumSet'

第二个命令的输出如下

@startuml

interface java.lang.Cloneable
interface java.lang.Iterable
interface java.io.Serializable
interface java.util.Collection
interface java.util.Set
abstract java.util.AbstractCollection
abstract java.util.EnumSet
abstract java.util.AbstractSet
class java.util.RegularEnumSet
java.lang.Iterable <|-- java.util.Collection
java.util.Collection <|.. java.util.AbstractCollection
java.util.Collection <|-- java.util.Set
java.util.AbstractCollection <|-- java.util.AbstractSet
java.util.Set <|.. java.util.AbstractSet
java.util.AbstractSet <|-- java.util.EnumSet
java.lang.Cloneable <|.. java.util.EnumSet
java.io.Serializable <|.. java.util.EnumSet
java.util.EnumSet <|-- java.util.RegularEnumSet

@enduml

借助 IntelliJ IDEA (Community Edition) 中的 PlantUML 插件,就能将上述 puml 转化为对应的类图。

参考资料