背景
大家在日常的开发过程中,应该用到过 java.util.EnumSet。在 java.util.EnumSet#noneOf 这个静态方法中,我们可以看到如下的逻辑
下方的两个类都是 EnumSet 的子类。
java.util.RegularEnumSetjava.util.JumboEnumSet
java.util.EnumSet#noneOf 方法所返回的对象的精确类型分为两种情况 ⬇️
- 当枚举类 中的元素个数 满足 时,
java.util.EnumSet#noneOf方法会返回java.util.RegularEnumSet的一个实例 - 当枚举类 中的元素个数 满足 时,
java.util.EnumSet#noneOf方法会返回java.util.JumboEnumSet的一个实例
在 java.util.RegularEnumSet 中,我们可以找到一个名为 elements 的字段 ⬇️
我们可以通过反射来观察,java.util.RegularEnumSet 增减元素时,elements 字段的变化。
注意事项
完整代码 里用到了 switch 箭头语法,编译运行需 JDK 14 及以上(我写本文时用的是 JDK 21)。
正文
我借助 trae 生成了可以观察 java.util.RegularEnumSet 中 elements 字段变化的代码。为了行文连贯,我把完整的代码放到 完整代码 这一小节里了
如何运行
请将 完整代码 保存为 EnumSetCliDemo.java。使用如下命令可以编译 EnumSetCliDemo.java。
javac EnumSetCliDemo.java
编译之后,会得到以下 class 文件
EnumSetCliDemo.classEnumSetCliDemo$Action.classEnumSetCliDemo$Planet.class
使用如下命令,可以运行 EnumSetCliDemo 中的 main 方法。(通过反射读取 RegularEnumSet 的私有 elements 字段,受 JDK 9+ 模块系统限制,需要使用 --add-opens 选项)
java --add-opens=java.base/java.util=ALL-UNNAMED EnumSetCliDemo
运行效果示例
初始效果
Planet 枚举类型
Planet 枚举类型的代码如下 ⬇️
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() 方法 ⬇️
注意:重复 add 已存在的元素、remove 不存在的元素、或对空集合执行 clear,位掩码都不会发生变化,程序会输出对应的 no-op 提示。
查看当前状态
我们可以随时执行 show 命令来查看当前的状态,包括 elements 字段的十六进制值、size、集合内容以及按位展示的表格 ⬇️
表格中各列按 ordinal 降序排列:最左侧是 ordinal 最大的 NEPTUNE,最右侧是 ordinal 为 0 的 MERCURY。这样是为了与上方 elements 十六进制值"高位在左"的顺序保持一致 —— val 行从左往右读,正好对应 elements 从高位到低位的每一个二进制位。
该命令还有一个别名 state,两者效果相同。
查看帮助
我们可以执行 help 命令来查看该程序支持的所有命令 ⬇️
退出
通过执行 quit 命令(或者它的别名 exit)可以退出该程序。
完整代码
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.
* <p>
* 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"),
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 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 INVALID -> 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 INVALID.
*/
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 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.util.RegularEnumSet 的类图
如果忽略泛型,我们可以为 java.util.RegularEnumSet 画出以下类图 ⬇️ (以下类图基于 JDK 21)
如何为 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 转化为类图。
参考资料
- [Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)(我先写了 GUI 版本的文章,后来想到 GUI 的代码太多,于是又写了本文)
- [Java] 如何自动生成简单的 PlantUML 类图(我绘制
java.util.RegularEnumSet的类图参考了这篇文章)