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

下方的两个类都是 EnumSet 的子类。
java.util.RegularEnumSetjava.util.JumboEnumSet
java.util.EnumSet#noneOf 方法所返回的对象的精确类型分为两种情况 ⬇️
- 当枚举类 E 中的元素个数 n 满足 n≤64 时,
java.util.EnumSet#noneOf方法会返回java.util.RegularEnumSet的一个实例 - 当枚举类 E 中的元素个数 n 满足 n>64 时,
java.util.EnumSet#noneOf方法会返回java.util.JumboEnumSet的一个实例
在 java.util.RegularEnumSet 中,我们可以找到一个名为 elements 的字段 ⬇️

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

我们可以通过反射来观察,java.util.RegularEnumSet 中增减元素时,elements 字段的变化。
正文
我借助 trae 生成了可以观察 java.util.RegularEnumSet 中 elements 字段变化的代码。为了行文连贯,我把完整的代码放到 完整代码 这一小节里了
如何运行
请将 完整代码 保存为 EnumSetCliDemo.java.java。使用如下命令可以编译 EnumSetCliDemo.java.java。
bash
javac EnumSetCliDemo.java
编译之后,会得到以下 class 文件
EnumSetCliDemo.classEnumSetCliDemo$Action.classEnumSetCliDemo$Planet.class
使用如下命令,可以运行 EnumSetCliDemo 中的 main 方法。
bash
java --add-opens=java.base/java.util=ALL-UNNAMED EnumSetCliDemo
运行效果示例
初始效果

Planet 枚举类型
Planet 枚举类型的代码如下 ⬇️
java
enum Planet { MERCURY, VENUS, EARTH, MARS, JUPITER, SATURN, URANUS, NEPTUNE }
程序启动后,会执行 EnumSet.noneOf(Planet.class) 这样的逻辑。我们在这个基础上,可以对 EnumSet 进行操作。

添加元素
我们可以通过指定 name 或者 ordinal,向 EnumSet 中添加对应的元素

最终执行的是 java.util.RegularEnumSet 中的 add(E e) 方法 ⬇️

移除元素
我们可以通过指定 name 或者 ordinal,从 EnumSet 中移除对应的元素

最终执行的是 java.util.RegularEnumSet 中的 remove(E e) 方法 ⬇️


清空 EnumSet
我们可以执行 clear 命令来清空 EnumSet

最终执行的是 java.util.RegularEnumSet 中的 clear() 方法 ⬇️

退出
通过执行 quit 命令可以退出该程序。

完整代码
java
import java.lang.reflect.Field;
import java.util.EnumSet;
import java.util.Scanner;
/**
* Pure CLI counterpart of EnumSetBehaviorDemo: it lets you add/remove
* constants of a small enum and prints the internal "elements" bitmask
* after every change.
*
* Run with (the "elements" field is private, hence --add-opens):
* javac EnumSetCliDemo.java
* java --add-opens=java.base/java.util=ALL-UNNAMED EnumSetCliDemo
*/
public class EnumSetCliDemo {
enum Planet { MERCURY, VENUS, EARTH, MARS, JUPITER, SATURN, URANUS, NEPTUNE }
/** Every command word the REPL accepts, including aliases. */
private enum Action {
ADD("add"),
REMOVE("remove"),
CLEAR("clear"),
SHOW("show", "state"),
HELP("help"),
QUIT("quit", "exit");
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 Planet[] planets = Planet.values();
private final EnumSet<Planet> set = EnumSet.noneOf(Planet.class);
public static void main(String[] args) {
new EnumSetCliDemo().run();
}
private void run() {
printHelp();
System.out.println();
System.out.println(" start: EnumSet.noneOf(Planet.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 null -> System.out.println(" unknown command: \"" + cmd
+ "\" (type help for the command list)");
}
}
}
}
private void add(String arg) {
Planet p = parseConstant(arg);
if (p == null) {
printUnknownConstant(arg);
return;
}
reportChange(p, true, set.add(p));
}
private void remove(String arg) {
Planet p = parseConstant(arg);
if (p == null) {
printUnknownConstant(arg);
return;
}
reportChange(p, false, set.remove(p));
}
private void printUnknownConstant(String arg) {
System.out.println(" unknown constant: \"" + arg
+ "\" (use a name or an ordinal 0-" + (planets.length - 1) + ")");
}
/** Prints which bit changed (or why it did not), then the new state. */
private void reportChange(Planet p, boolean add, boolean changed) {
System.out.println();
System.out.println(" " + (add ? "add" : "remove") + "(Planet." + p.name() + ")");
if (changed) {
String op = add
? "|= 1L << " + p.ordinal()
: "&= ~(1L << " + p.ordinal() + ")";
System.out.println(" elements " + op + " (bit " + p.ordinal()
+ (add ? " set by OR)" : " cleared by AND NOT)"));
} else {
System.out.println(" no-op: bit " + p.ordinal()
+ (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: all bits were already 0" : ""));
printState();
System.out.println();
}
/** Maps a command word (already lower-cased) to its Action, or null. */
private Action parseAction(String cmd) {
for (Action action : Action.values()) {
if (action.matches(cmd)) {
return action;
}
}
return null;
}
/** Accepts a constant name (case-insensitive) or its ordinal. */
private Planet parseConstant(String arg) {
if (arg.isEmpty()) {
return null;
}
for (Planet p : planets) {
if (p.name().equalsIgnoreCase(arg)) {
return p;
}
}
try {
int ordinal = Integer.parseInt(arg);
if (ordinal >= 0 && ordinal < planets.length) {
return planets[ordinal];
}
} catch (NumberFormatException ignored) {
// not a number: fall through to null
}
return null;
}
/** elements, size and membership, followed by the bit-by-bit table. */
private void printState() {
long e = elements();
System.out.printf(" elements %s%n", toHex(e));
System.out.printf(" size %d%n", set.size());
System.out.printf(" set %s%n", set);
System.out.println();
printBitTable(e);
}
/**
* Same information as the GUI's bit grid: one column per constant, in
* descending bit order so it lines up with the binary string.
*/
private void printBitTable(long e) {
StringBuilder ordinals = new StringBuilder(" ordinal ");
StringBuilder names = new StringBuilder(" name ");
StringBuilder vals = new StringBuilder(" val ");
for (int i = planets.length - 1; i >= 0; i--) {
ordinals.append(String.format("%-8s", i));
names.append(String.format("%-8s", planets[i].name()));
vals.append(String.format("%-8s", ((e >> i) & 1L) == 1L ? "1" : "0"));
}
System.out.println(ordinals);
System.out.println(names);
System.out.println(vals);
}
private long elements() {
try {
Field f = set.getClass().getDeclaredField("elements");
f.setAccessible(true);
return f.getLong(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<Planet> CLI demo");
System.out.println("enum Planet { MERCURY, VENUS, EARTH, MARS, JUPITER, SATURN, URANUS, NEPTUNE }");
System.out.println();
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 用 GUI 展示 EnumSet 增减元素时的内部变化(上) (我先写了 GUI 版本的文章,后来想到 GUI 的代码太多,于是又写了本文)