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

55 阅读5分钟

背景

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

image.png

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

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

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

  • 当枚举类 EE 中的元素个数 nn 满足 n≤64n \le 64 时,java.util.EnumSet#noneOf 方法会返回 java.util.RegularEnumSet 的一个实例
  • 当枚举类 EE 中的元素个数 nn 满足 n>64n \gt 64 时,java.util.EnumSet#noneOf 方法会返回 java.util.JumboEnumSet 的一个实例

在 java.util.RegularEnumSet 中,我们可以找到一个名为 elements 的字段 ⬇️

image.png

我们可以通过反射来观察,java.util.RegularEnumSet 增减元素时,elements 字段的变化。

注意事项

完整代码 里用到了 switch 箭头语法,编译运行需 JDK 14 及以上(我写本文时用的是 JDK 21)。

image.png

正文

我借助 trae 生成了可以观察 java.util.RegularEnumSet 中 elements 字段变化的代码。为了行文连贯,我把完整的代码放到 完整代码 这一小节里了

如何运行

请将 完整代码 保存为 EnumSetCliDemo.java。使用如下命令可以编译 EnumSetCliDemo.java。

javac EnumSetCliDemo.java

编译之后,会得到以下 class 文件

  • EnumSetCliDemo.class
  • EnumSetCliDemo$Action.class
  • EnumSetCliDemo$Planet.class

使用如下命令,可以运行 EnumSetCliDemo 中的 main 方法。(通过反射读取 RegularEnumSet 的私有 elements 字段,受 JDK 9+ 模块系统限制,需要使用 --add-opens 选项)

java --add-opens=java.base/java.util=ALL-UNNAMED EnumSetCliDemo

运行效果示例

初始效果

image.png

Planet 枚举类型

Planet 枚举类型的代码如下 ⬇️

enum Planet { MERCURY, VENUS, EARTH, MARS, JUPITER, SATURN, URANUS, NEPTUNE }

程序启动后,会执行 EnumSet.noneOf(Planet.class) 这样的逻辑。我们在这个基础上,可以对 EnumSet 进行操作。

image.png

添加元素

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

image.png

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

image.png

移除元素

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

image.png

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

image.png

image.png

清空 EnumSet

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

image.png

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

image.png

注意:重复 add 已存在的元素、remove 不存在的元素、或对空集合执行 clear,位掩码都不会发生变化,程序会输出对应的 no-op 提示。

查看当前状态

我们可以随时执行 show 命令来查看当前的状态,包括 elements 字段的十六进制值、size、集合内容以及按位展示的表格 ⬇️

image.png

表格中各列按 ordinal 降序排列:最左侧是 ordinal 最大的 NEPTUNE,最右侧是 ordinal 为 0 的 MERCURY。这样是为了与上方 elements 十六进制值"高位在左"的顺序保持一致 —— val 行从左往右读,正好对应 elements 从高位到低位的每一个二进制位。

该命令还有一个别名 state,两者效果相同。

查看帮助

我们可以执行 help 命令来查看该程序支持的所有命令 ⬇️

image.png

退出

通过执行 quit 命令(或者它的别名 exit)可以退出该程序。

image.png

完整代码

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)

image.png

如何为 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 转化为类图。

参考资料