背景
在 [Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上) 这篇文章里,我们借助 GUI 工具观察了 java.util.RegularEnumSet:当枚举类型中的常量数量 满足 时,EnumSet.noneOf 方法会返回 java.util.RegularEnumSet 的实例。java.util.RegularEnumSet 中用一个 long elements 字段存放位图。
本篇来看另一种情况:当枚举类型中的常量数量 满足 时,EnumSet.noneOf 返回的是 java.util.JumboEnumSet 的实例 ⬇️
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[],数组长度是 ,也就是(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。使用如下命令可以编译它。
javac JumboEnumSetBehaviorDemo.java
编译之后,会得到以下 class 文件
JumboEnumSetBehaviorDemo.classJumboEnumSetBehaviorDemo$BitGrid.classJumboEnumSetBehaviorDemo$Tag.class
使用如下命令,可以运行 JumboEnumSetBehaviorDemo 中的 main 方法。
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,因为 。
位与 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]都是0xFFFF_FFFF_FFFF_FFFF(64 位全 1),elements[2]为3EnumSet.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_0000C70在elements[1]的bit 6(),所以elements[1]的低7位(bit 0~bit 6)被置为1,结果是0x0000_0000_0000_007F- 不涉及
elements[2]中的bit,elements[2]的值仍为0 size直接设为70 - 60 + 1 = 11
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(...),就能观察到被修改的那个 long 元素的变化。
下面这张图依次添加了 C0、C63、C64、C69 ⬇️
添加之后:
C0→elements[0]的bit 0C63→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也成立 - 只有当这个
long元素的值真的变了,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。删除操作只会影响一个 long 元素。
clear() 按钮
点击 clear() 会清空整个集合。JumboEnumSet 的 clear() 会调用 Arrays.fill(elements, 0L),把 elements 中的所有元素置为 0,同时把 size 设为 0。
与 RegularEnumSet 直接把 elements 赋为 0 不同,这里要遍历数组逐个清零。
Operation log 面板
界面底部的 Operation log 面板会记录每一次操作。启动时的日志如下 ⬇️
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 long elements; bits run 63 -> 0 left to right, like the hex text above
第一行提醒您:在调用工厂方法之前,add(...)、remove(...) 和 clear() 都是禁用的。从 what to observe: 开始,它列出了这个工具可以辅助您观察的几条规律,最后两行正是本篇的两个关键点:ordinal 如何拆成"long 元素下标 + bit 偏移",以及方格图为什么与十六进制文本同向。
此后每执行一次操作,日志会追加一行,并自动滚动到最新一行。每一行分为三列:
- 该次调用的表达式,例如
add(Tag.C128) elements的变化。对于add(...)/remove(...),只显示被修改的那个long元素,写法为"操作前" -> "操作后";对于noneOf,写的是elements = long[3], all zero;对于allOf,则直接列出各个 long 元素的值- 中括号内的一句说明,解释这次操作对位做了什么
以 noneOf 和 add(Tag.C128) 为例 ⬇️
EnumSet.noneOf(Tag.class) elements = long[3], all zero [allocated eagerly: no lazy init like ArrayList]
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)]
另外,range 会先打印一行区间说明(含实际跨过的 long 元素数),再为涉及的每个 long 元素打印一行当前值;clear() 没有"前后值"可显示,所以只保留说明。
Reset 按钮
Reset 是 demo 自身的辅助按钮,不对应任何 Java API。点击后会丢弃当前集合、清空 Operation log,并重新输出一段以 --- demo reset: call a factory method first --- 开头的启动日志;控件回到初始状态(工厂方法可用,add(...)/remove(...)/clear() 再次禁用)。
RegularEnumSet 与 JumboEnumSet 对比
| 维度 | RegularEnumSet | JumboEnumSet |
|---|---|---|
| 适用条件 | 枚举常量数 | |
| 位图字段 | long elements | long[] elements(长度为 ) |
常量 i 的位置 | elements 的 bit i | elements[i >>> 6] 的 bit i & 63 |
size() | 每次调用 Long.bitCount(elements) 现算 | 直接返回维护的 size 字段 |
add(...)/remove(...) | 对 elements 做一次位运算 | 先定位到某个 long 元素,再做位运算 |
range | 一个掩码即可覆盖整个区间 | 跨元素时分成起始、中间、结束三段处理 |
clear() | elements = 0 | Arrays.fill(elements, 0L),同时 size = 0 |
| 方格图 | 一行 64 格 | 每个 long 元素一行,末行可能不满 64 格 |
具体选择哪个实现,发生在创建集合的那一刻:EnumSet.noneOf、allOf、range 都会根据枚举常量的个数决定返回 RegularEnumSet 还是 JumboEnumSet,实例一旦创建就不会在两种实现之间切换。
对使用者来说,两个子类暴露的 EnumSet API 完全一致,差异只体现在内部的位图组织和少数操作的实现方式上——比如 size 是现算还是维护、clear 是给一个 long 赋值还是清空整个数组。回到这两篇文章的方格图:上篇的 RegularEnumSet 只有一行,本篇的 JumboEnumSet 有三行、最后一行只用到 2 格,这就是两者最直观的区别。
完整代码
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 LONG ELEMENT: 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 long elements (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 LONG_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;
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[] longLabels;
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() {
// ---- UI ----
JFrame 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 factoryMethodRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
factoryMethodRow.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)");
factoryMethodRow.add(rowCaption("factory method", CODE_COLOR));
factoryMethodRow.add(noneOfButton);
factoryMethodRow.add(allOfButton);
control.add(factoryMethodRow);
// ---------- 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();
longLabels = new JLabel[LONG_COUNT];
for (int w = 0; w < LONG_COUNT; w++) {
longLabels[w] = new JLabel();
}
sizeLabel = new JLabel();
membershipLabel = new JLabel();
JLabel[] lines = new JLabel[2 + LONG_COUNT + 2];
lines[0] = runtimeClassLabel;
lines[1] = elementsLabel;
System.arraycopy(longLabels, 0, lines, 2, LONG_COUNT);
lines[2 + LONG_COUNT] = sizeLabel;
lines[3 + LONG_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-long 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)", LONG_COUNT,
"allocated eagerly: no lazy init like ArrayList"));
refreshView();
}
private void doAllOf() {
clearError();
set = EnumSet.allOf(Tag.class);
lastTouchedBit = -1;
setSetCreatedState();
long[] e = longs();
logLine(String.format("%-40s elements[0]=%s, elements[1]=%s, elements[2]=%s",
"EnumSet.allOf(Tag.class)", fmtLong(e[0]), fmtLong(e[1]), fmtLong(e[2])));
logLine(String.format("%-40s [%s]", "",
"two full long elements (-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 = longs();
int fromLong = from.ordinal() >>> 6;
int toLong = to.ordinal() >>> 6;
int longSpan = toLong - fromLong + 1;
logLine(String.format("%-40s contiguous bits %d..%d set across %d long element%s:",
opText, from.ordinal(), to.ordinal(), longSpan, longSpan == 1 ? "" : "s"));
for (int w = fromLong; w <= toLong; w++) {
logLine(String.format("%-40s elements[%d] = %s", "", w, fmtLong(e[w])));
}
refreshView();
}
private void doAdd() {
clearError();
Tag sel = (Tag) elementCombo.getSelectedItem();
long[] before = longs().clone();
boolean changed = set.add(sel);
long[] after = longs();
// 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, fmtLong(before[w]), fmtLong(after[w]), note));
refreshView();
}
private void doRemove() {
clearError();
Tag sel = (Tag) elementCombo.getSelectedItem();
long[] before = longs().clone();
boolean changed = set.remove(sel);
long[] after = longs();
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, fmtLong(before[w]), fmtLong(after[w]), note));
refreshView();
}
private void doClear() {
clearError();
set.clear();
lastTouchedBit = -1;
logLine(String.format("%-40s [%s]", "clear()",
"Arrays.fill(elements, 0L): all " + LONG_COUNT + " long elements 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 long elements; 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[] longs() {
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 < LONG_COUNT; w++) {
longLabels[w].setText(stateLine("elements[" + w + "]", "--"));
}
sizeLabel.setText(stateLine("size", "--"));
membershipLabel.setText(stateLine("set", "--"));
} else {
long[] e = longs();
runtimeClassLabel.setText(stateLine("runtime class", set.getClass().getName()));
elementsLabel.setText(stateLine("elements", "long[" + e.length + "]"));
for (int w = 0; w < e.length; w++) {
longLabels[w].setText(stateLine("elements[" + w + "]", fmtLong(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 fmtLong(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 long element, 64 columns; row w = elements[w]
// =====================================================================
/**
* Draws one row per long element 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: each long 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, LONG_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[LONG_COUNT] : longs();
for (int w = 0; w < LONG_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: this long 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 >= LONG_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 + ")";
}
}
}