背景
在 上一篇文章 里我们提到 EnumSet.noneOf 方法所返回的对象的精确类型取决于枚举常量个数 :
- 时,返回
java.util.RegularEnumSet的实例- 它有
private long elements字段
- 它有
- 时,返回
java.util.JumboEnumSet的实例
上一篇只观察了前者,这一篇我们来观察后者。先查看 java.util.JumboEnumSet 中的字段 ⬇️
也就是说,常量个数超过 64 之后,位图从一个 long 变成了 long 数组。约定:ordinal 为 i 的常量,位于 elements[i >>> 6] 的第 i & 63 位。这里 >>> 6 就是右移 6 位(相当于用整数除法除以 64),& 63 就是对 64 取余。
顺带能看出第二个差异:RegularEnumSet 没有 size 字段(size() 方法直接返回 Long.bitCount(elements)),而 JumboEnumSet 显式维护了一个 int size 字段。这一点在 size 从算出来变成记下来 这一小节会提到。
正文
我借助 trae 生成了可以观察 java.util.JumboEnumSet 中 elements 字段变化的代码。为了行文连贯,完整的代码会在 完整代码 这一小节提供。
演示用的枚举类型 Tag 里有 130 个常量,需要 3 个 long 才能表示(),因此 elements 是长度为 3 的 long 数组,它的最后一个 long 元素只会用到 2 位。
/** 130 constants: ordinals 0..129 span three 64-bit words (word 2 uses only 2 bits). */
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
}
如何运行
请将 完整代码 保存为 JumboEnumSetCliDemo.java。使用如下命令可以编译该文件(代码在 JDK 17 上编译通过)。
javac JumboEnumSetCliDemo.java
编译之后,会得到以下 class 文件 ⬇️
JumboEnumSetCliDemo.classJumboEnumSetCliDemo$Action.classJumboEnumSetCliDemo$Tag.class
使用如下命令,可以运行 JumboEnumSetCliDemo 中的 main 方法。通过反射读取 JumboEnumSet 的私有 elements 字段,会受到 JDK 9+ 模块系统限制,需要使用 --add-opens 选项 ⬇️
java --add-opens=java.base/java.util=ALL-UNNAMED JumboEnumSetCliDemo
运行效果示例
初始效果
程序启动后会执行 EnumSet.noneOf(Tag.class),此时 elements 已经是一个长度为 3 的全零数组 ⬇️
可以看到 class 一行显示的是 JumboEnumSet,这和前文的分析一致。
添加元素
我们可以通过指定 name 或者 ordinal,向 EnumSet 中添加对应的元素。最终执行的是 java.util.JumboEnumSet 中的 add(E e) 方法 ⬇️
先添加 C0(ordinal 0),它和 elements[0] 的 bit 0 对应 ⬇️
再添加 C63(ordinal 63),它和 elements[0] 的 bit 63(即该 long 元素的最高位)对应 ⬇️
接着添加 C64(ordinal 64)。注意:它和 elements[1] 的 bit 0 对应 ⬇️
再添加 C69(ordinal 69),它和 elements[1] 的 bit 5 对应()⬇️
0x21 就是二进制 0b100001,正好对应 bit 5 和 bit 0。
如果重复添加一个已经存在的元素,add 返回 false,数组不会有任何变化 ⬇️
最后再试试涉及 elements[2] 的操作。添加 C128(ordinal 128)。128 >>> 6 = 2,它和 elements[2] 的 bit 0 对应 ⬇️
elements[2] 从 0x0000_0000_0000_0000 变成 0x0000_0000_0000_0001。因为 130 = 2 × 64 + 2,只有 ordinal 128 和 129 会映射到第三个 long,所以这个元素最多只会用到低 2 位。
移除元素
我们可以通过指定 name 或者 ordinal,从 EnumSet 中移除对应的元素。最终执行的是 java.util.JumboEnumSet 中的 remove(Object) 方法 ⬇️
在刚才操作的基础上,移除 C64,效果如下 ⬇️
0x21 变成 0x20:bit 0 被清掉,bit 5 保留。
清空 EnumSet
我们可以执行 clear 命令来清空 EnumSet。
最终执行的是 java.util.JumboEnumSet 中的 clear() 方法。和 RegularEnumSet 里的 elements = 0 不同,这里要把数组里每个 long 都清零,同时把 size 变为 0 ⬇️
退出
通过执行 quit 命令可以退出该程序。另外,exit 是 quit 命令的别名。
源码里值得一提的细节
移位距离不需要对 64 取模
add 方法的逻辑如下 ⬇️
注意到 elements[eWordNum] |= (1L << eOrdinal); 里写的是 1L << eOrdinal,而不是 1L << (eOrdinal & 63)。当 ordinal 是 64 时,按字面理解 1L << 64 似乎会把 1 移动到 64 位的范围之外,但它实际落在 elements[1] 的 bit 0,也就是 1L << 0 的结果。
原因在于 Java 语言对 long 的移位指令只会用到右操作数的低 6 位,所以 1L << 64 等价于 1L << 0、1L << 69 等价于 1L << 5。
在 The Java® Language Specification 的 15.19. Shift Operators 中可以找到如下描述
If the promoted type of the left-hand operand is
long, then only the six lowest-order bits of the right-hand operand are used as the shift distance. It is as if the right-hand operand were subjected to a bitwise logical AND operator&(§15.22.1) with the mask value0x3f(0b111111). The shift distance actually used is therefore always in the range0to63, inclusive.
size 从算出来变成记下来
RegularEnumSet 的 size() 方法只有一句 Long.bitCount(elements),因为统计一个 long 里有多少个 1 只花费常数时间。到了 JumboEnumSet 里,elements 是数组,统计位数就要遍历所有 long 元素,所以它改为在 add/remove 成功时直接对 size 字段加一减一 ⬇️
完整代码
完整的代码如下(在 JDK 17 上编译通过)⬇️
import java.lang.reflect.Field;
import java.util.EnumSet;
import java.util.Scanner;
/**
* CLI counterpart of EnumSetCliDemo, but for an enum with 130 constants.
* EnumSet then switches from RegularEnumSet (a single long)
* to JumboEnumSet, whose private "elements" field is a long[]: constant
* with ordinal i lives in elements[i >>> 6], at bit i & 63 of that entry.
*
* Run with (the "elements" field is private, hence --add-opens):
* javac JumboEnumSetCliDemo.java
* java --add-opens=java.base/java.util=ALL-UNNAMED JumboEnumSetCliDemo
*/
public class JumboEnumSetCliDemo {
/** 130 constants: ordinals 0..129 span three 64-bit words (word 2 uses only 2 bits). */
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 enum Action {
ADD("add"),
REMOVE("remove"),
CLEAR("clear"),
SHOW("show", "state"),
HELP("help"),
QUIT("quit", "exit"),
INVALID();
private final String[] words;
Action(String... words) {
this.words = words;
}
boolean matches(String text) {
for (String word : words) {
if (word.equals(text)) {
return true;
}
}
return false;
}
}
private final Tag[] tags = Tag.values();
private final EnumSet<Tag> set = EnumSet.noneOf(Tag.class);
public static void main(String[] args) {
new JumboEnumSetCliDemo().run();
}
private void run() {
printHelp();
System.out.println();
System.out.println(" start: EnumSet.noneOf(Tag.class)");
printState();
System.out.println();
try (Scanner in = new Scanner(System.in)) {
COMMAND_LOOP:
while (true) {
System.out.print("> ");
System.out.flush();
if (!in.hasNextLine()) {
break;
}
String line = in.nextLine().trim();
if (line.isEmpty()) {
continue;
}
String[] parts = line.split("\\s+", 2);
String cmd = parts[0].toLowerCase();
String arg = parts.length > 1 ? parts[1].trim() : "";
switch (parseAction(cmd)) {
case ADD -> add(arg);
case REMOVE -> remove(arg);
case CLEAR -> clear();
case SHOW -> {
System.out.println();
printState();
System.out.println();
}
case HELP -> printHelp();
case QUIT -> {
break COMMAND_LOOP;
}
case INVALID -> System.out.println(" unknown command: \"" + cmd
+ "\" (type help for the command list)");
}
}
}
}
private void add(String arg) {
Tag t = parseConstant(arg);
if (t == null) {
printUnknownConstant(arg);
return;
}
reportChange(t, true, set.add(t));
}
private void remove(String arg) {
Tag t = parseConstant(arg);
if (t == null) {
printUnknownConstant(arg);
return;
}
reportChange(t, false, set.remove(t));
}
private void printUnknownConstant(String arg) {
System.out.println(" unknown constant: \"" + arg
+ "\" (use a name or an ordinal 0-" + (tags.length - 1) + ")");
}
/** Prints which word/bit changed (or why it did not), then the new state. */
private void reportChange(Tag t, boolean add, boolean changed) {
int ordinal = t.ordinal();
int word = ordinal >>> 6; // ordinal / 64
int bit = ordinal & 63; // ordinal % 64
System.out.println();
System.out.println(" " + (add ? "add" : "remove") + "(Tag." + t.name() + ")");
if (changed) {
String op = add
? "|= 1L << " + bit
: "&= ~(1L << " + bit + ")";
System.out.println(" elements[" + word + "] " + op
+ " (ordinal " + ordinal + " -> elements[" + word + "], bit " + bit
+ (add ? " set by OR)" : " cleared by AND NOT)"));
} else {
System.out.println(" no-op: elements[" + word + "], bit " + bit
+ (add ? " was already set" : " was not set"));
}
printState();
System.out.println();
}
private void clear() {
boolean wasEmpty = set.isEmpty();
set.clear();
System.out.println();
System.out.println(" clear()"
+ (wasEmpty ? " no-op: every elements[i] was already 0" : ""));
printState();
System.out.println();
}
/** Returns Action.INVALID if the word matches no command (or alias). */
private Action parseAction(String cmd) {
for (Action action : Action.values()) {
if (action.matches(cmd)) {
return action;
}
}
return Action.INVALID;
}
/** Accepts a constant name (case-insensitive) or its ordinal. */
private Tag parseConstant(String arg) {
if (arg.isEmpty()) {
return null;
}
for (Tag t : tags) {
if (t.name().equalsIgnoreCase(arg)) {
return t;
}
}
try {
int ordinal = Integer.parseInt(arg);
if (ordinal >= 0 && ordinal < tags.length) {
return tags[ordinal];
}
} catch (NumberFormatException ignored) {
// not a number: fall through to null
}
return null;
}
/** Runtime class, size, membership, then one line per entry of elements. */
private void printState() {
long[] e = elements();
System.out.printf(" %-13s %s%n", "class", set.getClass().getSimpleName());
System.out.printf(" %-13s %d%n", "size", set.size());
System.out.printf(" %-13s %s%n", "set", set);
System.out.printf(" %-13s long[%d]%n", "elements", e.length);
for (int w = 0; w < e.length; w++) {
int lo = w * 64;
int hi = Math.min(lo + 63, tags.length - 1);
System.out.printf(" elements[%d] %s (ordinals %d..%d)%n", w, toHex(e[w]), lo, hi);
}
}
/** JumboEnumSet's "elements" is a long[]; read it via reflection. */
private long[] elements() {
try {
Field f = set.getClass().getDeclaredField("elements");
f.setAccessible(true);
return (long[]) f.get(set);
} catch (ReflectiveOperationException ex) {
throw new IllegalStateException(ex);
}
}
/** Full 64 bits as 16 hex digits, grouped every 4: 0x0000_0000_0000_0001. */
private String toHex(long v) {
String raw = String.format("%016X", v);
StringBuilder builder = new StringBuilder("0x");
for (int i = 0; i < raw.length(); i++) {
if (i > 0 && i % 4 == 0) {
builder.append('_');
}
builder.append(raw.charAt(i));
}
return builder.toString();
}
private void printHelp() {
System.out.println("EnumSet<Tag> CLI demo (130 constants -> JumboEnumSet, long[3] elements)");
System.out.println("commands:");
System.out.println(" add <name|ordinal> set.add(...)");
System.out.println(" remove <name|ordinal> set.remove(...)");
System.out.println(" clear set.clear()");
System.out.println(" show print the current state");
System.out.println(" help show this help");
System.out.println(" quit exit");
}
}
类图
如果忽略泛型,java.util.JumboEnumSet 的简要类图如下 ⬇️
如何为 java.util.JumboEnumSet 绘制类图?
我使用了 [Java] 如何自动生成简单的 PlantUML 类图 一文中提供的代码。关键的命令如下
javac ClassDiagramGenerator.java
java ClassDiagramGenerator 'java.util.JumboEnumSet'
第二个命令的输出如下
@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.JumboEnumSet
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.JumboEnumSet
@enduml
借助 IntelliJ IDEA (Community Edition) 中的 PlantUML 插件,就能将上述 puml 转化为类图。