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

背景

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

下方的两个类都是 EnumSet 的子类。

  • java.util.RegularEnumSet
  • java.util.JumboEnumSet

java.util.EnumSet#noneOf 方法所返回的对象的精确类型分为两种情况 ⬇️

  • 当枚举类 EE E 中的元素个数 nn n 满足 n≤64n \le 64 n≤64 时,java.util.EnumSet#noneOf 方法会返回 java.util.RegularEnumSet 的一个实例
  • 当枚举类 EE E 中的元素个数 nn n 满足 n>64n \gt 64 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.class
  • EnumSetCliDemo$Action.class
  • EnumSetCliDemo$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");
    }
}

参考资料

相关推荐
qq_436962181 小时前
金蝶云星空数据分析怎么做?
java·数据库·数据分析
代码山河1 小时前
键盘输入与输出:Scanner与System.out详解
java·学习·架构·教程·面向对象·项目
碧海银沙音频科技研究院1 小时前
基于炬芯ATS308X的BLE 无线方式离线日志传输导出功能实现
java·linux·服务器
YY362952381 小时前
Caravel 收费吗?是开源的吗?能离线用吗?关于 Caravel 的 6 个常见误会
人工智能·ai编程·claude code
denlaku1 小时前
JDK 20 新特性详解
java
tedcloud1231 小时前
Docker Compose 部署 Memos:从安装到备份恢复
linux·开发语言·jvm·后端·docker·容器·rust
木井巳1 小时前
【JavaEE】SpringBoot 统一功能处理: 拦截器、统一返回与全局异常
java·spring boot·后端·spring·java-ee
pan_junbiao2 小时前
Go语言中的空白标识符
开发语言·后端·golang