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

背景

在 Java 用 GUI 展示 EnumSet 增减元素时的内部变化(上) 这篇文章里,我们借助 GUI 工具观察了 java.util.RegularEnumSet:当枚举类型中的常量数量 nn n 满足 n≤64n \le 64 n≤64 时,EnumSet.noneOf 方法会返回 java.util.RegularEnumSet 的实例。java.util.RegularEnumSet 中用一个 long elements 字段存放位图。

本篇来看另一种情况:当枚举类型中的常量数量 nn n 满足 n>64n\gt 64 n>64 时,EnumSet.noneOf 返回的是 java.util.JumboEnumSet 的实例 ⬇️

java 复制代码
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
    Enum<?>[] universe = getUniverse(elementType);
    if (universe == null)
        throw new ClassCastException(elementType + " not an enum");

    if (universe.length <= 64)
        return new RegularEnumSet<>(elementType, universe);
    else
        return new JumboEnumSet<>(elementType, universe);
}

JumboEnumSet 中有 elements 字段和 size 字段 ⬇️

与 RegularEnumSet 相比,有两处关键变化:

  • elements 字段从单个 long 变成了 long[],数组长度是 ⌈n64⌉ \lceil \frac{n}{64} \rceil ⌈64n⌉,也就是 (n + 63) >>> 6
  • 多了一个 int size 字段,add / remove 时会同步维护这个字段的值。RegularEnumSet 没有这个字段,它的 size() 是通过 Long.bitCount(elements) 实时计算的

本篇演示用的枚举类型 Tag 里有 130 个常量(C0 到 C129),所以 elements 是 long[3],elements[2] 只用到最低的 2 位。

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

注意事项

  • 完整代码用到了 instanceof 模式匹配,需要 JDK 16 及以上(写作本文时,我使用的是 JDK 21)
  • 反射读取 JumboEnumSet 的私有 elements 字段,受 JDK 9+ 模块系统限制,运行时需要使用 --add-opens 选项

正文

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

如何运行

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

bash 复制代码
javac JumboEnumSetBehaviorDemo.java

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

  • JumboEnumSetBehaviorDemo.class
  • JumboEnumSetBehaviorDemo$BitGrid.class
  • JumboEnumSetBehaviorDemo$Tag.class

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

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

运行效果示例

初始效果

窗口标题里的 (130 constants, long[3]) 已经点明了本篇的关键:130 个常量,elements 数组中会有 3 个元素。

控件一共四行,左侧的小字是行标题:

  • factory method:EnumSet.noneOf(Tag.class)、EnumSet.allOf(Tag.class)
  • factory method:EnumSet.range(...),以及它的 from/to 两个参数
  • java API:选择 element 之后执行 add(...)、remove(...),以及 clear()
  • demo only:Reset,仅用于重置演示

蓝色按钮对应真实的 Java API 调用;深灰色表示 demo 自身的辅助控件;浅灰色表示当前不可点击。

在调用工厂方法之前,add(...)/remove(...)/clear()/Reset 都是禁用的。此时 elements 还不存在,状态面板显示 --,方格图里的取值也全是 ?。

internal state of the EnumSet 面板

界面中部的 internal state of the EnumSet 面板会展示 elements 字段的当前值,每次操作后都会刷新。以 EnumSet.allOf(Tag.class) 为例,该面板会显示 7 行信息(其中 elements[0] ~ elements[2] 占三行)⬇️

  • runtime class:set 的运行时类型,即 java.util.JumboEnumSet。这一行可以直接印证前面"背景"小节中的结论------ noneOf 返回的确实是 JumboEnumSet 的实例
  • elements:long[3],这里是数组的长度
  • elements[0]/elements[1]/elements[2]:每个 long 元素的十六进制形式,固定 16 位,每 4 位用 _ 分组
  • size:set 的大小。注意这里显示的是 JumboEnumSet 维护的 size 字段,而 RegularEnumSet 会用 Long.bitCount(elements) 实时计算
  • set:EnumSet 中当前包含的枚举常量。130 个元素全在里面时字符串很长,面板会截断到 150 个字符并以 ... 结尾

点击 EnumSet.allOf(Tag.class) 按钮之后,会看到如下的效果 ⬇️

elements[0]/elements[1] 的 64 位全为 1;elements[2] 只有最低的 2 位为 1,因为 130=2×64+2130 = 2 \times 64 + 2 130=2×64+2。

位与 ordinal 的对应关系

elements 依然是一个位掩码(bitmask),只不过现在按一段 64 位切成了多个 long:ordinal 为 i 的常量,位于 elements[i >>> 6] 的第 i & 63 位,也就是 elements[i / 64] 的第 i % 64 位。

对 130 个常量来说,映射关系如下 ⬇️

数组元素 存放的常量 用到的 bit
elements[0] C0 ~ C63 bit 63 ~ bit 0
elements[1] C64 ~ C127 bit 63 ~ bit 0
elements[2] C128 ~ C129 bit 1 ~ bit 0(其余 62 位没有对应常量)

面板下方的方格图正是按这个规则画的:elements 中的每个元素一行,每行 64 格。

每一行都像上面的十六进制文本一样,从左到右依次是 bit 63、bit 62、......、bit 0 ,即最高位在左、最低位在右。这样方格图就能和 elements[w] 的十六进制文本直接对齐。另外,每 4 格有一道深色分隔线,正好对应十六进制里的一个数字。

以第一行为例:最左边的格子是 bit 63(即 C63),最右边是 bit 0(即 C0);行首的 63..0 标出了这一行覆盖的 bit 范围。

第三行只用到最右边两格(C128、C129),左侧 62 格画成虚线:这些 bit 在 long 里确实存在,但没有枚举常量映射到它们。

当集合还没有创建时,格子里的取值显示为 ?;创建之后,1 表示属于集合,0 表示不属于。最近一次真正改变了状态的那一位会以橙色边框标出;如果操作没有改变任何位(例如重复 add 已存在的元素),则不会高亮任何位,上一次的高亮也会被清除。

调用工厂方法

Tag 是固定不变的 130 个常量,所以这个 demo 没有再提供"枚举类型"下拉框。三个静态工厂方法如下 ⬇️

  • EnumSet.noneOf(Tag.class):得到 elements = long[3],三个元素全为 0。分配是立即发生的,没有 ArrayList 空参构造那样的懒初始化
  • EnumSet.allOf(Tag.class):elements[0]/elements[1] 都是 0xFFFFFFFF(64 位全 1),elements[2] 为 3
  • EnumSet.range(Tag.C60, Tag.C70):把一段连续的 ordinal 置为 1(您也可以选择其他的起止元素)

对于 range,可以按 long 拆开理解。以 Tag.C60 到 Tag.C70 为例:

  • C60 在 elements[0] 的 bit 60,所以 elements[0] 的高 4 位(bit 60 ~ bit 63)被置为 1,结果是 0xF000_0000_0000_0000
  • C70 在 elements[1] 的 bit 6(70 - 64 = 6),所以 elements[1] 的低 7 位(bit 0 ~ bit 6)被置为 1,结果是 0x0000_0000_0000_007F
  • 中间的 elements[2] 没有涉及,仍为 0
  • size 直接设为 70 - 60 + 1 = 11

JumboEnumSet 的 addRange 在跨 long 时会分三段处理:起始 word 从 from 所在位一直填到该 word 的最高位,中间整段 word 直接设为 -1,结束 word 从最低位填到 to 所在位;当 from 与 to 落在同一个 word 时,只处理这一个 word。最后直接把 size 设为区间长度。

range 参数非法时

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

例如执行 EnumSet.range(Tag.C70, Tag.C10) 时,界面会有三处反馈 ⬇️

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

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

添加元素

选择 element 之后点击 add(...),就能观察到被修改的那个 word 的前后变化。

下面这张图依次添加了 C0、C63、C64、C69 ⬇️

添加之后:

  • C0 → elements[0] 的 bit 0
  • C63 → elements[0] 的 bit 63(第一行最左边的格子)
  • C64 → elements[1] 的 bit 0(第二行最右边的格子)
  • C69 → elements[1] 的 bit 5

C63 和 C64 是相邻的两个常量,但它们分别落在两个 long 元素的相反两端,方格图上会看到高亮从第一行最左侧跳到第二行最右侧。

JumboEnumSet 的 add 方法如下 ⬇️

这里有两点值得注意:

  • 先算出 eWordNum = eOrdinal >>> 6,再对第 eOrdinal 位做或运算。由于 JVM 对 long 移位的距离隐式按 64 取模,1L << 128 等价于 1L << 0,所以这句对 C128 也成立
  • 只有当这个 word 的值真的变了,size 才会加 1;重复添加已存在的元素时 size 不变,add 返回 false

再添加 C128,就会落在 elements[2] 的范围内 ⬇️

日志里只会显示被修改的那个 long 元素,例如:

注意第三列里写的是 1L << 0 而不是 1L << 128------日志按"long 元素下标 + 该 long 元素内的 bit"来表述,和您在方格图上看到的位置直接对应。

删除元素

删除与添加是对称的:elements[ordinal >>> 6] &= ~(1L << ordinal)。以 remove(Tag.C64) 为例 ⬇️

日志里记录的是这次变化的前后值:elements[1] 0x0000_0000_0000_0021 -> 0x0000_0000_0000_0020。C64 是 elements[1] 的 bit 0,清零后整个元素从 0x21 变成了 0x20。remove(...) 操作(至多)只会影响一个 long 元素。

clear() 按钮

点击 clear() 会清空整个集合。JumboEnumSet 的 clear() 会调用 Arrays.fill(elements, 0L),把 elements 中的所有元素置为 0,同时把 size 设为 0。

与 RegularEnumSet 直接把 elements 赋为 0 不同,这里要遍历数组逐个清零。

Operation log 面板

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

text 复制代码
  add(...), remove(...) and clear() are disabled until a factory method is called
what to observe:
  EnumSet.noneOf(Tag.class) -> elements = long[3], all zero (eager allocation)
  EnumSet.allOf(Tag.class)  -> elements[0]=elements[1]=-1, elements[2]=3
  add(e)                    -> elements[ordinal >>> 6] |= 1L << ordinal
  remove(e)                 -> elements[ordinal >>> 6] &= ~(1L << ordinal)
  clear()                   -> Arrays.fill(elements, 0L); size = 0
  ordinal i lives in elements[i / 64], at bit i % 64
  grid rows are words; bits run 63 -> 0 left to right, like the hex text above

第一行提醒您:在调用工厂方法之前,add(...)、remove(...) 和 clear() 都是禁用的。从 what to observe: 开始,它列出了这个工具可以辅助您观察的几条规律,最后两行正是本篇的两个关键点:ordinal 如何拆成"word 下标 + bit 偏移",以及方格图为什么与十六进制文本同向。

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

  1. 该次调用的表达式,例如 add(Tag.C128)
  2. elements 的变化。对于 add(...)/remove(...),只显示被修改的那个 long 元素,写法为"操作前" -> "操作后";对于 noneOf,写的是 elements = long[3], all zero;对于 allOf,则直接列出各个 long 元素的值
  3. 中括号内的一句说明,解释这次操作对位做了什么

以 allOf 和 add(Tag.C128) 为例 ⬇️

text 复制代码
EnumSet.allOf(Tag.class)                 elements[0]=0xFFFF_FFFF_FFFF_FFFF, elements[1]=0xFFFF_FFFF_FFFF_FFFF, elements[2]=0x0000_0000_0000_0003
                                         [two full words (-1 = all 64 bits set), then (1L << 2) - 1 = 3]
add(Tag.C128)                            elements[2] 0x0000_0000_0000_0000 -> 0x0000_0000_0000_0001 [elements[2] |= 1L << 0  (ordinal 128 -> elements[2], bit 0 set by OR)]
clear()                                  [Arrays.fill(elements, 0L): all 3 words set to 0, and size = 0]

另外,range 会先打印一行区间说明(含实际跨过的 word 数),再为涉及的每个 word 打印一行当前值;clear() 没有「前后值」可显示,所以只保留说明。

Reset 按钮

Reset 是 demo 自身的辅助按钮,不对应任何 Java API。点击后会丢弃当前集合、清空 Operation log,并重新输出一段以 --- demo reset: call a factory method first --- 开头的启动日志;控件回到初始状态(工厂方法可用,add(...)/remove(...)/clear() 再次禁用)。

完整代码

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.awt.event.MouseEvent;
import java.lang.reflect.Field;
import java.util.EnumSet;

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

/**
 * Swing counterpart of JumboEnumSetCliDemo: it visualizes the internal state
 * of java.util.JumboEnumSet while operations are executed. The demo enum Tag
 * has 130 constants (> 64), so the private "elements" field is a long[3]:
 * the constant with ordinal i lives in elements[i >>> 6], at bit i & 63.
 * <p>
 * The bit grid draws ONE ROW PER WORD: row w shows the 64 bits of
 * elements[w], MSB-first like the hex text, so the cell at display column
 * c is ordinal w*64 + (63 - c). The last row maps only 2 constants
 * (130 = 2*64 + 2); its remaining 62 slots are drawn as unused.
 * <p>
 * Observable facts:
 * 1) EnumSet.noneOf(Tag.class) -> elements = long[3], all zero (allocated
 *    eagerly, no lazy init like ArrayList's empty-arg constructor);
 * 2) EnumSet.allOf(Tag.class)  -> elements[0] = elements[1] = -1,
 *    elements[2] = (1L << 2) - 1 = 3;
 * 3) add(e)    -> elements[w] |= 1L << ordinal      (w = ordinal >>> 6);
 * 4) remove(e) -> elements[w] &= ~(1L << ordinal);
 * 5) clear()   -> Arrays.fill(elements, 0L), and size = 0;
 * 6) size is a maintained field here (RegularEnumSet computes bitCount).
 * <p>
 * The "elements" field is private, so reflection is required. On JDK 9+:
 * javac JumboEnumSetBehaviorDemo.java
 * java --add-opens=java.base/java.util=ALL-UNNAMED JumboEnumSetBehaviorDemo
 */
public class JumboEnumSetBehaviorDemo {

    /** 130 constants: ordinals 0..129 span three 64-bit words (row 2 uses only 2 bits). */
    private enum Tag {
        C0,  C1,  C2,  C3,  C4,  C5,  C6,  C7,  C8,  C9,
        C10, C11, C12, C13, C14, C15, C16, C17, C18, C19,
        C20, C21, C22, C23, C24, C25, C26, C27, C28, C29,
        C30, C31, C32, C33, C34, C35, C36, C37, C38, C39,
        C40, C41, C42, C43, C44, C45, C46, C47, C48, C49,
        C50, C51, C52, C53, C54, C55, C56, C57, C58, C59,
        C60, C61, C62, C63, C64, C65, C66, C67, C68, C69,
        C70, C71, C72, C73, C74, C75, C76, C77, C78, C79,
        C80, C81, C82, C83, C84, C85, C86, C87, C88, C89,
        C90, C91, C92, C93, C94, C95, C96, C97, C98, C99,
        C100, C101, C102, C103, C104, C105, C106, C107, C108, C109,
        C110, C111, C112, C113, C114, C115, C116, C117, C118, C119,
        C120, C121, C122, C123, C124, C125, C126, C127, C128, C129
    }

    private static final Tag[] TAGS = Tag.values();
    private static final int TAG_COUNT = TAGS.length;                 // 130
    private static final int WORD_COUNT = (TAG_COUNT + 63) >>> 6;     // 3

    // ---- shared design language with RegularEnumSetBehaviorDemo ----
    /** 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<Tag> set;
    private int lastTouchedBit = -1;

    // ---- UI ----
    private JFrame frame;
    private JButton noneOfButton;
    private JButton allOfButton;
    private JButton addButton;
    private JButton removeButton;
    private JButton clearButton;
    private JButton resetButton;
    private JButton rangeButton;
    private JComboBox<Tag> elementCombo;
    private JComboBox<Tag> rangeFromCombo;
    private JComboBox<Tag> rangeToCombo;
    private JPanel rangeGroup;
    private JLabel errorBanner;

    private JLabel runtimeClassLabel;
    private JLabel elementsLabel;
    private JLabel[] wordLabels;
    private JLabel sizeLabel;
    private JLabel membershipLabel;
    private JTextArea logArea;
    private BitGrid bitGrid;

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

    private JumboEnumSetBehaviorDemo() {
        createAndShowGui();
    }

    private void createAndShowGui() {
        frame = new JFrame("JumboEnumSet internals demo (130 constants, long[3])");
        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 1: static factory methods ----------
        JPanel ctorRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
        ctorRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        noneOfButton = new JButton("EnumSet.noneOf(Tag.class)");
        allOfButton = new JButton("EnumSet.allOf(Tag.class)");
        noneOfButton.setFont(CODE_FONT);
        allOfButton.setFont(CODE_FONT);
        noneOfButton.setToolTipText("Runs: EnumSet.noneOf(Tag.class)");
        allOfButton.setToolTipText("Runs: EnumSet.allOf(Tag.class)");
        ctorRow.add(rowCaption("factory method", CODE_COLOR));
        ctorRow.add(noneOfButton);
        ctorRow.add(allOfButton);
        control.add(ctorRow);

        // ---------- row 2: EnumSet.range with its two arguments ----------
        rangeFromCombo = new JComboBox<>(TAGS);
        rangeToCombo = new JComboBox<>(TAGS);
        rangeToCombo.setSelectedItem(Tag.C1);
        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);

        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 3: Java API calls ----------
        JPanel opRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
        opRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        addButton = new JButton();
        removeButton = new JButton();
        clearButton = new JButton("clear()");
        elementCombo = new JComboBox<>(TAGS);
        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()");

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

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

        runtimeClassLabel = new JLabel();
        elementsLabel = new JLabel();
        wordLabels = new JLabel[WORD_COUNT];
        for (int w = 0; w < WORD_COUNT; w++) {
            wordLabels[w] = new JLabel();
        }
        sizeLabel = new JLabel();
        membershipLabel = new JLabel();

        JLabel[] lines = new JLabel[2 + WORD_COUNT + 2];
        lines[0] = runtimeClassLabel;
        lines[1] = elementsLabel;
        System.arraycopy(wordLabels, 0, lines, 2, WORD_COUNT);
        lines[2 + WORD_COUNT] = sizeLabel;
        lines[3 + WORD_COUNT] = membershipLabel;
        for (JLabel line : lines) {
            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);

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

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

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

        setNoSetState();
        printStartupLog();
        refreshView();

        frame.setSize(1330, 720);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    // =====================================================================
    // Operations (each logs the per-word before/after of "elements")
    // =====================================================================

    private void doNoneOf() {
        clearError();
        set = EnumSet.noneOf(Tag.class);
        lastTouchedBit = -1;
        setSetCreatedState();
        logLine(String.format("%-40s elements = long[%d], all zero [%s]",
                "EnumSet.noneOf(Tag.class)", WORD_COUNT,
                "allocated eagerly: no lazy init like ArrayList"));
        refreshView();
    }

    private void doAllOf() {
        clearError();
        set = EnumSet.allOf(Tag.class);
        lastTouchedBit = -1;
        setSetCreatedState();
        long[] e = words();
        logLine(String.format("%-40s elements[0]=%s, elements[1]=%s, elements[2]=%s",
                "EnumSet.allOf(Tag.class)", fmtWord(e[0]), fmtWord(e[1]), fmtWord(e[2])));
        logLine(String.format("%-40s [%s]", "",
                "two full words (-1 = all 64 bits set), then (1L << 2) - 1 = 3"));
        refreshView();
    }

    private void doRange() {
        Tag from = (Tag) rangeFromCombo.getSelectedItem();
        Tag to = (Tag) rangeToCombo.getSelectedItem();
        String opText = "EnumSet.range(Tag." + from.name() + ", Tag." + to.name() + ")";
        try {
            set = EnumSet.range(from, to);
        } catch (IllegalArgumentException ex) {
            // EnumSet.range requires from.ordinal() <= to.ordinal()
            logLine(String.format("%-40s threw %s", opText, ex));
            showError(opText + " failed: IllegalArgumentException: from.ordinal() ("
                    + from.ordinal() + ") must be <= to.ordinal() (" + to.ordinal() + ")");
            return;
        }
        clearError();
        lastTouchedBit = -1;
        setSetCreatedState();
        long[] e = words();
        int fromWord = from.ordinal() >>> 6;
        int toWord = to.ordinal() >>> 6;
        int wordSpan = toWord - fromWord + 1;
        logLine(String.format("%-40s contiguous bits %d..%d set across %d word%s:",
                opText, from.ordinal(), to.ordinal(), wordSpan, wordSpan == 1 ? "" : "s"));
        for (int w = fromWord; w <= toWord; w++) {
            logLine(String.format("%-40s elements[%d] = %s", "", w, fmtWord(e[w])));
        }
        refreshView();
    }

    private void doAdd() {
        clearError();
        Tag sel = (Tag) elementCombo.getSelectedItem();
        long[] before = words().clone();
        boolean changed = set.add(sel);
        long[] after = words();
        // 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;

        int ordinal = sel.ordinal();
        int w = ordinal >>> 6;
        int bit = ordinal & 63;
        String note = changed
                ? "elements[" + w + "] |= 1L << " + bit
                  + "  (ordinal " + ordinal + " -> elements[" + w + "], bit " + bit + " set by OR)"
                : "no-op: elements[" + w + "], bit " + bit + " was already set";
        logLine(String.format("%-40s elements[%d] %s -> %s [%s]",
                "add(Tag." + sel.name() + ")", w, fmtWord(before[w]), fmtWord(after[w]), note));
        refreshView();
    }

    private void doRemove() {
        clearError();
        Tag sel = (Tag) elementCombo.getSelectedItem();
        long[] before = words().clone();
        boolean changed = set.remove(sel);
        long[] after = words();
        lastTouchedBit = changed ? sel.ordinal() : -1;

        int ordinal = sel.ordinal();
        int w = ordinal >>> 6;
        int bit = ordinal & 63;
        String note = changed
                ? "elements[" + w + "] &= ~(1L << " + bit + ")"
                  + "  (ordinal " + ordinal + " -> elements[" + w + "], bit " + bit + " cleared by AND NOT)"
                : "no-op: elements[" + w + "], bit " + bit + " was not set";
        logLine(String.format("%-40s elements[%d] %s -> %s [%s]",
                "remove(Tag." + sel.name() + ")", w, fmtWord(before[w]), fmtWord(after[w]), note));
        refreshView();
    }

    private void doClear() {
        clearError();
        set.clear();
        lastTouchedBit = -1;
        logLine(String.format("%-40s [%s]", "clear()",
                "Arrays.fill(elements, 0L): all " + WORD_COUNT + " words set to 0, and size = 0"));
        refreshView();
    }

    private void resetDemo() {
        set = null;
        lastTouchedBit = -1;
        setNoSetState();
        clearError();
        logArea.setText("");
        logLine("--- demo reset: call a factory method first ---");
        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(Tag.class) -> elements = long[3], all zero (eager allocation)");
        logLine("  EnumSet.allOf(Tag.class)  -> elements[0]=elements[1]=-1, elements[2]=3");
        logLine("  add(e)                    -> elements[ordinal >>> 6] |= 1L << ordinal");
        logLine("  remove(e)                 -> elements[ordinal >>> 6] &= ~(1L << ordinal)");
        logLine("  clear()                   -> Arrays.fill(elements, 0L); size = 0");
        logLine("  ordinal i lives in elements[i / 64], at bit i % 64");
        logLine("  grid rows are words; bits run 63 -> 0 left to right, like the hex text above");
        logLine("");
    }

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

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

    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() {
        Tag sel = (Tag) elementCombo.getSelectedItem();
        if (sel != null && addButton != null && removeButton != null) {
            addButton.setText("add(Tag." + sel.name() + ")");
            removeButton.setText("remove(Tag." + sel.name() + ")");
        }
    }

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

    // =====================================================================
    // Reflection access to JumboEnumSet.elements (a long[])
    // =====================================================================

    private long[] words() {
        try {
            Field f = set.getClass().getDeclaredField("elements");
            f.setAccessible(true);
            return (long[]) f.get(set);
        } catch (ReflectiveOperationException e) {
            throw new IllegalStateException("cannot read JumboEnumSet.elements", e);
        }
    }

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

    private void refreshView() {
        if (set == null) {
            runtimeClassLabel.setText(stateLine("runtime class", "--"));
            elementsLabel.setText(stateLine("elements", "-- (call a factory method first)"));
            for (int w = 0; w < WORD_COUNT; w++) {
                wordLabels[w].setText(stateLine("elements[" + w + "]", "--"));
            }
            sizeLabel.setText(stateLine("size", "--"));
            membershipLabel.setText(stateLine("set", "--"));
        } else {
            long[] e = words();
            runtimeClassLabel.setText(stateLine("runtime class", set.getClass().getName()));
            elementsLabel.setText(stateLine("elements", "long[" + e.length + "]"));
            for (int w = 0; w < e.length; w++) {
                wordLabels[w].setText(stateLine("elements[" + w + "]", fmtWord(e[w])));
            }
            sizeLabel.setText(stateLine("size",
                    set.size() + "  (maintained field; RegularEnumSet would compute Long.bitCount)"));
            membershipLabel.setText(stateLine("set", truncate(String.valueOf(set), 150)));
        }
        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);
    }

    /** Full 64 bits as 16 hex digits, grouped every 4: 0x0000_0000_0000_0001. */
    private static String fmtWord(long v) {
        String raw = String.format("%016X", v);
        StringBuilder sb = new StringBuilder("0x");
        for (int i = 0; i < raw.length(); i++) {
            if (i > 0 && i % 4 == 0) {
                sb.append('_');
            }
            sb.append(raw.charAt(i));
        }
        return sb.toString();
    }

    /** The set's toString can reach ~700 chars with 130 members; keep one line. */
    private static String truncate(String s, int max) {
        return s.length() <= max ? s : s.substring(0, max) + "...";
    }

    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 row per word, 64 columns; row w = elements[w]
    // =====================================================================

    /**
     * Draws one row per word of "elements". Like the hex text above the
     * grid, bits run MSB-first: the leftmost cell of row w is bit 63
     * (ordinal w*64+63), the rightmost is bit 0 (ordinal w*64). A darker
     * separator every 4 cells mirrors the '_' grouping of the hex text, so
     * each hex digit lines up with its 4 cells. Slots beyond C129 in the
     * last row are drawn dashed: the word is 64 bits wide, but only 2 of
     * them map to constants.
     */
    private final class BitGrid extends JPanel {
        private static final int LABEL_W = 96;
        private static final int MAX_CELL_W = 17;
        private static final int MIN_CELL_W = 9;
        private static final int CELL_H = 30;
        private static final int GAP = 2;
        private static final int ROW_GAP = 6;

        BitGrid() {
            int w = LABEL_W + 64 * (MAX_CELL_W + GAP) + GAP;
            setPreferredSize(new Dimension(w, WORD_COUNT * (CELL_H + ROW_GAP) + ROW_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);

            int cellW = Math.max(MIN_CELL_W,
                    Math.min(MAX_CELL_W, (getWidth() - LABEL_W - GAP - 63 * GAP) / 64));
            long[] e = (set == null) ? new long[WORD_COUNT] : words();

            for (int w = 0; w < WORD_COUNT; w++) {
                int rowY = ROW_GAP + w * (CELL_H + ROW_GAP);
                g.setColor(Color.DARK_GRAY);
                g.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11));
                g.drawString("elements[" + w + "]", GAP, rowY + 14);
                g.setColor(new Color(150, 150, 150));
                g.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 9));
                int lo = w * 64;
                int hi = Math.min(lo + 63, TAG_COUNT - 1);
                g.drawString(hi + ".." + lo, GAP, rowY + 27);

                // display column p shows bit (63 - p): MSB on the left,
                // matching the hex text in the state panel
                for (int p = 0; p < 64; p++) {
                    int bit = 63 - p;
                    int ordinal = w * 64 + bit;
                    int x = LABEL_W + GAP + p * (cellW + GAP);
                    if (ordinal >= TAG_COUNT) {
                        // unused slot: the word has 64 bits, but no constant maps here
                        g.setColor(new Color(210, 210, 210));
                        g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
                                BasicStroke.JOIN_MITER, 10, new float[]{3, 3}, 0));
                        g.drawRect(x, rowY, cellW, CELL_H);
                        continue;
                    }
                    boolean bitSet = set != null && ((e[w] >>> bit) & 1L) == 1L;

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

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

                    // ordinal (top) and bit value (bottom)
                    g.setColor(bitSet ? Color.WHITE : Color.DARK_GRAY);
                    g.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 8));
                    FontMetrics fm = g.getFontMetrics();
                    String ord = String.valueOf(ordinal);
                    g.drawString(ord, x + (cellW - fm.stringWidth(ord)) / 2, rowY + 11);
                    g.setFont(new Font(Font.MONOSPACED, Font.BOLD, 12));
                    fm = g.getFontMetrics();
                    String bitText = set == null ? "?" : bitSet ? "1" : "0";
                    g.drawString(bitText, x + (cellW - fm.stringWidth(bitText)) / 2, rowY + 27);
                }

                // a darker separator every 4 cells mirrors the '_' grouping
                // of the hex text: each hex digit lines up with its 4 cells
                g.setColor(new Color(130, 130, 130));
                g.setStroke(new BasicStroke(1));
                for (int p = 3; p + 1 < 64; p += 4) {
                    int gx = LABEL_W + GAP + (p + 1) * (cellW + GAP) - GAP / 2 - 1;
                    g.drawLine(gx, rowY - 1, gx, rowY + CELL_H + 1);
                }
            }
        }

        @Override
        public String getToolTipText(MouseEvent event) {
            int cellW = Math.max(MIN_CELL_W,
                    Math.min(MAX_CELL_W, (getWidth() - LABEL_W - GAP - 63 * GAP) / 64));
            int w = (event.getY() - ROW_GAP) / (CELL_H + ROW_GAP);
            int p = (event.getX() - LABEL_W - GAP) / (cellW + GAP);
            if (w < 0 || w >= WORD_COUNT || p < 0 || p >= 64) {
                return null;
            }
            int bit = 63 - p;
            int ordinal = w * 64 + bit;
            if (ordinal >= TAG_COUNT) {
                return "elements[" + w + "] bit " + bit + ": unused (no constant with ordinal " + ordinal + ")";
            }
            return "Tag.C" + ordinal + " = elements[" + w + "] bit " + bit
                    + "  (ordinal " + ordinal + " = " + w + " * 64 + " + bit + ")";
        }
    }
}

参考资料

相关推荐
AI人工智能+电脑小能手1 小时前
一次请求打几个下游,这个Java并行任务工具类6个方法按序聚合
java·线程池·并发编程·1024程序员节·批量处理·并行任务
Mk598782 小时前
给 Agent 装上对话框 —— AgentScope-Java 接入 Spring Boot 与 SSE 流式实战
后端
付威20232 小时前
从 Loop 到 Runtime:rpi Agent Loop 与 Cordis 的边界、取舍与组合
后端
IT_陈寒2 小时前
Redis的LRU淘汰策略坑了我一整天
前端·人工智能·后端
孔令飞3 小时前
工具分享:OneX AI 工具安装、配置及功能介绍
云原生·go·ai编程
paopaokaka_luck3 小时前
心理咨询管理系统(WebSocket实时沟通、基于用户的协同过滤、 ECharts多维分析、 预约业务流程、预约校验与定时处理)
java·vue.js·spring boot·mysql·spring·echarts·1024程序员节
2301_32241428043 小时前
springboot农产品交易平台的设计与实现08055-计算机毕设 原创(免费领源码+部署教程+开发环境)
java·spring boot·redis·mysql·tomcat·maven·mybatis
Tangyuewei3 小时前
AI 编程后,越来越不会码了
人工智能·ai编程
slacker-kian3 小时前
[工程]:智能招聘--一个可组装的人才智能推荐系统
ai·ai编程·向量数据库·1024程序员节·相似性搜索·人力推荐系统