[Java] 用 CLI 展示 EnumMap 增删键值对时的内部变化

背景

在之前的文章里,我们分别用 GUI 和 CLI 观察了 java.util.EnumSet

  • 当枚举类型中的常量个数不超过 64 时,RegularEnumSet 用 long elements 字段存放位图
  • 当枚举类型中的常量个数超过 64 时,JumboEnumSet 用 long[] elements 保存位图

在 EnumSet 里,ordinal 用来定位一个 bit。java.util.EnumMap 的思路相同,但用法更直接------ordinal 就是数组下标。

在 EnumMap 的源码中可以看到以下字段 ⬇️

这五个字段对应的可观察事实:

  • keyType:构造时传入的枚举类,是 final 的,创建后不再变化
  • keyUniverse:该枚举类的全部常量,下标就是 ordinal
  • vals:与 keyUniverse 对应的值数组,vals[i] 就是常量 keyUniverse[i] 对应的值
  • size:当前键值对个数
  • NULL:一个私有的静态哨兵对象,用来代替 null 存进 vals

构造函数的逻辑比较直观 ⬇️

在构造函数里,会为 vals 分配好空间。

与 EnumSet 对照,两者对 ordinal 的用法不同:

位图 下标
EnumSet ordinal 定位 long/long[] 中的某个 bit ---
EnumMap --- ordinal 直接作为 vals 数组的下标

我们可以通过反射来观察 java.util.EnumMap 增删键值对时,keyUniverse、vals 和 size 的变化。

Java 用 GUI 展示 EnumMap 增删键值对时的内部变化 一文用方格图中的格子与 vals 中的各个元素对应,用不同颜色来区分"空槽位/真实值/NULL 哨兵"。

本篇改用 CLI。对 EnumMap 来说,这个选择很自然:

  • 它的内部的 vals 数组和与之对应的 keyUniverse,没有 EnumSet 那种"按 64 位切成若干个 long 元素"的逻辑,逐行打印就能把结构完整表达出来
  • NULL 哨兵在文本里可以写成明确的字符串,不容易误读
  • 读者复制命令就能自己复现,适合对照源码阅读

注意事项

  • 完整代码用到了 record 和文本块,需要 JDK 16+ 及以上(写作本文时,我使用的是 JDK 21)
  • 反射读取 EnumMap 的私有字段(keyType、keyUniverse、vals、size、NULL),受 JDK 9+ 模块系统限制,运行时需要使用 --add-opens=java.base/java.util=ALL-UNNAMED;JDK 16 起默认禁止这类反射访问,缺少参数时 demo 会在打印提示后退出。

正文

我借助 trae 生成了可以观察 java.util.EnumMap 内部状态的 CLI 代码。为了行文连贯,我把完整的代码放到 完整代码 这一小节里了。

如何运行

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

bash 复制代码
javac EnumMapCliDemo.java

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

  • EnumMapCliDemo.class
  • EnumMapCliDemo$EnumType.class
  • EnumMapCliDemo$Planet.class
  • EnumMapCliDemo$Vowel.class
  • EnumMapCliDemo$Digit.class
  • EnumMapCliDemo$Month.class
  • EnumMapCliDemo$Direction.class

使用如下命令,可以运行 EnumMapCliDemo 中的 main 方法。

bash 复制代码
java --add-opens=java.base/java.util=ALL-UNNAMED EnumMapCliDemo

启动后会出现一个交互式循环,输入命令,按回车即可。也可以把命令通过管道输入,方便复现同一段输出:

bash 复制代码
printf 'new Vowel\nput A alpha\nexit\n' | java --add-opens=java.base/java.util=ALL-UNNAMED EnumMapCliDemo

命令一览

输入 help 可以随时查看全部命令 ⬇️

text 复制代码
commands:
  new <enum>          create the map; <enum> = Planet|Vowel|Digit|Month|Direction
  put <key> <value>   put a mapping; value `null` maps to null
  get <key>           get(key)
  containsKey <key>   containsKey(key)
  remove <key>        remove(key)
  clear               clear()
  show                print keyType / keyUniverse / vals / size
  reset               drop the map and start over
  help                this text
  exit                quit
note: values are always String; `null` is a literal, not a string.
slot display:
  null                           no mapping (vals[i] == null)
  NULL sentinel (mapped to null) the key is mapped to null (maskNull)
  "value"                        a real value

命令分成三类:

  • 对应真实 Java API 的:new、put、get、containsKey、remove、clear
  • demo 自身的辅助命令:show(只读,重新打印一次当前状态)、reset(丢弃当前的 map)
  • 元命令:help、exit

new 的枚举名和 put 的 key 都不区分大小写,new vowel、put u null 也能识别。值可以带空格(put MARS red giant),只有单独的 null 会被当作 null 字面量,而不是字符串 "null"。

在我的终端里,中文和英文混排时,字符会对不齐,所以在本文中,我刻意只使用纯英文的枚举类 ⬇️

  • Planet(包含 8 个常量)
  • Vowel(包含 5 个常量)
  • Digit(包含 10 个常量)
  • Month(包含 12 个常量)
  • Direction(包含 4 个常量)

初始效果

启动时 banner 会说明当前还没有 map,以及槽位的三种显示形式 ⬇️

text 复制代码
EnumMap CLI demo (java.util.EnumMap internals)
no map yet - run: new <enum>
type help for all commands
slot display: null = no mapping, NULL sentinel (mapped to null) = key mapped to null, "v" = a real value

在 new 之前,put / get / containsKey / remove / clear / show 都会提示先创建 map:

text 复制代码
> put A alpha
no map yet: run new <enum> first

调用构造函数:new

输入 new Vowel 后,会执行 new EnumMap<>(Vowel.class) 这样的逻辑,输出如下 ⬇️

text 复制代码
new EnumMap<>(Vowel.class)
  EnumMap<Vowel, String>   keyType = Vowel.class
  keyUniverse.length = 5   vals.length = 5   size = 0
    [0] A -> null
    [1] E -> null
    [2] I -> null
    [3] O -> null
    [4] U -> null

输出可以分成两层:

  • 前两行是字段值:keyType、keyUniverse.length、vals.length、size
  • 后面的 [i] 常量名 -> 槽位内容 是逐下标的明细,i 就是常量在枚举声明中的 ordinal

几个值得注意的点:

  • keyType 是 final 字段,它决定了 keyUniverse 的内容和 vals 的长度,所以放在第一行
  • keyUniverse.length 和 vals.length 总是相等,两个数组一一对应
  • size 此时为 0,但 vals 已经是长度为 5 的数组------空间在构造时就分配好了 ,后续 put 不需要扩容
  • 逐下标的明细里没有 keyUniverse 本身,因为它的内容就是左边那一列常量名

这一步也体现了 keyType 的作用:EnumMap 必须知道自己要装哪一种枚举,才能拿到 keyUniverse,进而确定 vals 的长度。

添加键值对:put(key, value)

输入 put A alpha 后,输出如下 ⬇️

text 复制代码
put(Vowel.A, "alpha")
  vals[0]: null -> "alpha"   size: 0 -> 1
  EnumMap<Vowel, String>   keyType = Vowel.class
  keyUniverse.length = 5   vals.length = 5   size = 1
    [0] A -> "alpha"
    [1] E -> null
    [2] I -> null
    [3] O -> null
    [4] U -> null

结果分三段:

  1. 第一行是调用表达式 put(Vowel.A, "alpha")
  2. 第二行指明被触碰的槽位及变化:vals[0]: null -> "alpha",以及 size 的变化
  3. 之后是刷新过的完整状态

A 的 ordinal 是 0,所以值被直接写进 vals[0],size 从 0 变成 1。整个过程没有 HashMap 中"扰动 + 定位 + 链表"那样的复杂逻辑。

再对同一个 key 执行一次 put,值会被覆盖,但 size 不变。以输入 put A another 为例,其输出如下 ⬇️

text 复制代码
put(Vowel.A, "another")
  vals[0]: "alpha" -> "another"   size unchanged (1)
  EnumMap<Vowel, String>   keyType = Vowel.class
  keyUniverse.length = 5   vals.length = 5   size = 1
    [0] A -> "another"
    [1] E -> null
    [2] I -> null
    [3] O -> null
    [4] U -> null

这一点对应 JDK 源码里的 put 方法 ⬇️

只有原来的槽位是 null 时,size 才会加 1。size: 0 -> 1 和 size unchanged (1) 这两种写法,直接体现了这部分逻辑。

添加键值对:put(key, null)

输入 put U null 后,输出如下 ⬇️

text 复制代码
put(Vowel.U, null)
  vals[4]: null -> NULL sentinel (mapped to null)   size: 1 -> 2
  note: maskNull stored the NULL sentinel; containsKey stays true
  EnumMap<Vowel, String>   keyType = Vowel.class
  keyUniverse.length = 5   vals.length = 5   size = 2
    [0] A -> "another"
    [1] E -> null
    [2] I -> null
    [3] O -> null
    [4] U -> NULL sentinel (mapped to null)

这是 EnumMap 里一个容易迷惑人的细节:put(key, null) 存的不是 null ,而是私有的 NULL 哨兵对象。maskNull / unmaskNull 两个方法负责这层转换 ⬇️

之所以要区分,是因为 vals[i] == null 必须表示"这个 key 没有映射",而"映射到了 null"需要另一个可区分的标记。否则 containsKey 就无法正确处理。这有点像"我有一个空的钱包"和"我连钱包也没有"的区别。

所以 demo 把槽位显示成三种形式:

  • null:vals[i] == null,没有映射
  • NULL sentinel (mapped to null):vals[i] 存的是哨兵,key 映射到了 null
  • "value":真实值

NULL sentinel (mapped to null) 写得比单独的 NULL 长,是刻意的:读者一眼就知道这不是 null,而是哨兵,而且它代表"有映射、值是 null"。

get 与 containsKey

执行 get U 命令,对应的输出如下

text 复制代码
get(Vowel.U) -> null   (unmaskNull(NULL): looks absent, but containsKey is true)

执行 containsKey U 命令,对应的输出如下

text 复制代码
containsKey(Vowel.U) -> true   (vals[4] != null: present, even if it is the NULL sentinel)

结果看起来矛盾,实际是一致的:

  • get 经过 unmaskNull,把哨兵还原成 null 再返回
  • containsKey 只判断 vals[4] != null,哨兵不是 null,所以返回 true

对一个从未存过的 key (以 E 为例)做同样的操作 ⬇️

text 复制代码
> get E
get(Vowel.E) -> null   (vals[1] == null: no mapping)
> containsKey E
containsKey(Vowel.E) -> false   (vals[1] == null: no mapping)

现在则是"真的没有这个 key":槽位本身就是 null,两个方法都给出"不存在"的结论。

括号里的说明直接写出了判据,方便把输出和源码对上:U 和 E 的 get 都返回 null,但只有 U 的槽位是哨兵,因此 containsKey 的结果才不同。

删除键值对:remove

输入 remove U,删掉的是那个映射到 null 的键值对 ⬇️

text 复制代码
> remove U
remove(Vowel.U) -> null
  vals[4]: NULL sentinel (mapped to null) -> null   size: 2 -> 1
  note: the slot held the NULL sentinel, so remove() returned null (unmaskNull)
  EnumMap<Vowel, String>   keyType = Vowel.class
  keyUniverse.length = 5   vals.length = 5   size = 1
    [0] A -> "another"
    [1] E -> null
    [2] I -> null
    [3] O -> null
    [4] U -> null

remove 的返回值同样经过 unmaskNull,所以看起来是 null,和"key 不存在"时的返回值一样。区分的方法只有一个:看 size 有没有变 。这里 size 从 2 变成 1,说明确实删掉了一条映射。

如果对一个本来就没有映射的 key 执行 remove,则什么都不会改变 ⬇️

text 复制代码
> remove I
remove(Vowel.I) -> null
  vals[2] was already null: no-op, size unchanged (1)
  EnumMap<Vowel, String>   keyType = Vowel.class
  keyUniverse.length = 5   vals.length = 5   size = 1
    [0] A -> "another"
    [1] E -> null
    [2] I -> null
    [3] O -> null
    [4] U -> null

注意这一行里没有 ->,也没有 vals[2]: ... -> ... 的变化行,只有一句 no-op。

clear()

输入 clear 会清空所有槽位 ⬇️

text 复制代码
> clear
clear()   Arrays.fill(vals, null); size: 1 -> 0
  EnumMap<Vowel, String>   keyType = Vowel.class
  keyUniverse.length = 5   vals.length = 5   size = 0
    [0] A -> null
    [1] E -> null
    [2] I -> null
    [3] O -> null
    [4] U -> null

源码对应 Arrays.fill(vals, null) 加上 size = 0。注意 vals 的 长度不会变 ------clear 只是把槽位填成 null,数组本身保留,下一次 put 不需要重新分配。

切换 key 枚举类型

new Month 会换一个枚举重新开始,这次有 12 个常量 ⬇️

text 复制代码
new EnumMap<>(Month.class)
  EnumMap<Month, String>   keyType = Month.class
  keyUniverse.length = 12   vals.length = 12   size = 0
    [ 0] JANUARY   -> null
    [ 1] FEBRUARY  -> null
    [ 2] MARCH     -> null
    [ 3] APRIL     -> null
    [ 4] MAY       -> null
    [ 5] JUNE      -> null
    [ 6] JULY      -> null
    [ 7] AUGUST    -> null
    [ 8] SEPTEMBER -> null
    [ 9] OCTOBER   -> null
    [10] NOVEMBER  -> null
    [11] DECEMBER  -> null

这里多出两处对齐细节,都是 CLI 特有的:

  • 下标右对齐 :[ 0] 到 [ 9] 前面补了一个空格,[11] 不会被挤到左边。补的位数按"最大下标的位数"决定,所以 10 个常量以内不会出现空格
  • 常量名左对齐补齐 :JANUARY 后面会被补齐到 SEPTEMBER 的宽度,箭头才能在同一个列上开始

补齐用的是 String.repeat,实现如下:

java 复制代码
private static String pad(String s, int width) {
    return s + " ".repeat(width - s.length());
}

[ 0] 里的空格只是对齐用的填充,不是数据的一部分。

show 与 reset

show 是只读命令,任何时候都可以重新打印一次当前状态,不改变 map:

text 复制代码
  EnumMap<Month, String>   keyType = Month.class
  keyUniverse.length = 12   vals.length = 12   size = 0
    [ 0] JANUARY   -> null
    [ 1] FEBRUARY  -> null
    [ 2] MARCH     -> null
    [ 3] APRIL     -> null
    [ 4] MAY       -> null
    [ 5] JUNE      -> null
    [ 6] JULY      -> null
    [ 7] AUGUST    -> null
    [ 8] SEPTEMBER -> null
    [ 9] OCTOBER   -> null
    [10] NOVEMBER  -> null
    [11] DECEMBER  -> null

每次 put / remove / clear 也会自动打印同样的内容,所以 show 主要用在「上一条命令的输出被滚屏冲掉」的时候。

reset 对应 GUI 篇的 Reset 按钮,不对应任何 Java API:

text 复制代码
> reset
map dropped (keyType, keyUniverse, vals, size are gone): run new <enum>

它把 map 置为 null,于是 keyType、keyUniverse、vals、size 一并失去引用,状态回到初始。此时 show 也只会提示还没有 map,而不是报错:

text 复制代码
> show
no map yet: run new <enum> first

完整代码

java 复制代码
import java.lang.reflect.Field;
import java.util.EnumMap;
import java.util.List;
import java.util.Scanner;

/**
 * Pure CLI counterpart of EnumMapBehaviorDemo: it observes the internal
 * state of java.util.EnumMap after each operation.
 * <p>
 * EnumMap keeps two parallel arrays: "keyUniverse" holds every constant of
 * the key enum, and "vals" holds the associated values. A key's ordinal is
 * used DIRECTLY as the array index - there is no hashing at all. The enum
 * class itself is stored in the final field "keyType" at construction and
 * never changes afterward. A private static sentinel object NULL stands in
 * for null values, so "no mapping" (slot == null) and "mapped to null"
 * (slot == NULL) stay distinguishable.
 * <p>
 * How the demo prints a slot:
 *   null                          - no mapping (vals[i] == null);
 *   NULL sentinel (mapped to null) - the key is mapped to null (the private
 *                                    NULL object is stored via maskNull);
 *   "value"                       - a real value.
 * <p>
 * Observable facts:
 * 1) new EnumMap<>(X.class) -> keyType = X.class (final), keyUniverse = all
 *    constants of X, vals = new Object[n], all null, size = 0 (allocated
 *    eagerly: no lazy init like ArrayList's empty-arg constructor);
 * 2) put(k, v) -> vals[k.ordinal()] = v (ordinal IS the index: no hashing);
 * 3) put(k, null) -> vals[k.ordinal()] = NULL (maskNull: the sentinel keeps
 *    containsKey true even though get(k) returns null);
 * 4) remove(k) -> vals[k.ordinal()] = null;
 * 5) clear() -> Arrays.fill(vals, null), and size = 0;
 * 6) size is a maintained field, not a recomputation.
 * <p>
 * The fields are private, so reflection is required. On JDK 9+ run with:
 * javac EnumMapCliDemo.java
 * java --add-opens=java.base/java.util=ALL-UNNAMED EnumMapCliDemo
 */
public class EnumMapCliDemo {

    // ---- selectable key enums ----

    /** The eight planets. */
    private enum Planet { MERCURY, VENUS, EARTH, MARS, JUPITER, SATURN, URANUS, NEPTUNE }

    /** The five vowels. */
    private enum Vowel { A, E, I, O, U }

    /** The ten digits. */
    private enum Digit { ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE }

    /** The twelve months. */
    private enum Month {
        JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE,
        JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER
    }

    /** The four cardinal directions. */
    private enum Direction { EAST, SOUTH, WEST, NORTH }

    private record EnumType(String displayName, Class<? extends Enum<?>> type) { }

    private static final List<EnumType> TYPES = List.of(
            new EnumType("Planet", Planet.class),
            new EnumType("Vowel", Vowel.class),
            new EnumType("Digit", Digit.class),
            new EnumType("Month", Month.class),
            new EnumType("Direction", Direction.class));

    // ---- reflection on java.util.EnumMap ----
    private static Field keyTypeField;
    private static Field keyUniverseField;
    private static Field valsField;
    private static Field sizeField;
    private static Field nullField;

    // ---- state ----
    /** null means "no map yet": operations are rejected until new <enum> runs. */
    private EnumMap<?, String> map;

    public static void main(String[] args) {
        try {
            initReflection();
        } catch (ReflectiveOperationException | RuntimeException e) {
            System.out.println("cannot read java.util.EnumMap private fields: " + e);
            System.out.println("on JDK 9+ run with: "
                    + "java --add-opens=java.base/java.util=ALL-UNNAMED EnumMapCliDemo");
            return;
        }
        new EnumMapCliDemo().run();
    }

    private static void initReflection() throws ReflectiveOperationException {
        keyTypeField = accessible("keyType");
        keyUniverseField = accessible("keyUniverse");
        valsField = accessible("vals");
        sizeField = accessible("size");
        nullField = accessible("NULL");
    }

    private static Field accessible(String name) throws ReflectiveOperationException {
        Field f = EnumMap.class.getDeclaredField(name);
        f.setAccessible(true);
        return f;
    }

    // ---- command loop ----

    private void run() {
        printBanner();
        Scanner in = new Scanner(System.in);
        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 command = parts[0].toLowerCase();
            String rest = parts.length > 1 ? parts[1].trim() : "";
            switch (command) {
                case "help", "h", "?" -> printHelp();
                case "new" -> doNew(rest);
                case "put" -> doPut(rest);
                case "get" -> doGet(rest);
                case "containskey", "contains" -> doContainsKey(rest);
                case "remove" -> doRemove(rest);
                case "clear" -> doClear();
                case "show", "s" -> show();
                case "reset" -> doReset();
                case "exit", "quit" -> {
                    System.out.println("bye");
                    return;
                }
                default -> System.out.println("unknown command: " + parts[0] + " (type help)");
            }
        }
    }

    private void printBanner() {
        System.out.println("""
                EnumMap CLI demo (java.util.EnumMap internals)
                no map yet - run: new <enum>
                type help for all commands
                slot display: null = no mapping, NULL sentinel (mapped to null) = key mapped to null, "v" = a real value
                """);
    }

    private void printHelp() {
        System.out.println("""
                commands:
                  new <enum>          create the map; <enum> = Planet|Vowel|Digit|Month|Direction
                  put <key> <value>   put a mapping; value `null` maps to null
                  get <key>           get(key)
                  containsKey <key>   containsKey(key)
                  remove <key>        remove(key)
                  clear               clear()
                  show                print keyType / keyUniverse / vals / size
                  reset               drop the map and start over
                  help                this text
                  exit                quit
                note: values are always String; `null` is a literal, not a string.
                slot display:
                  null                           no mapping (vals[i] == null)
                  NULL sentinel (mapped to null) the key is mapped to null (maskNull)
                  "value"                        a real value""");
    }

    // ---- operations ----

    private void doNew(String arg) {
        EnumType t = findType(arg);
        if (t == null) {
            System.out.println("unknown enum type: " + arg + " (valid: Planet, Vowel, Digit, Month, Direction)");
            return;
        }
        map = newMap(t.type());
        System.out.printf("new EnumMap<>(%s.class)%n", t.displayName());
        show();
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private EnumMap<?, String> newMap(Class<? extends Enum<?>> type) {
        return new EnumMap(type);
    }

    private void doPut(String rest) {
        if (!requireMap()) {
            return;
        }
        String[] kv = rest.split("\\s+", 2);
        if (kv.length < 2) {
            System.out.println("usage: put <key> <value>   (use `null` for a null value)");
            return;
        }
        Enum<?> key = findKey(kv[0]);
        if (key == null) {
            return;
        }
        String raw = kv[1].trim();
        String value = "null".equalsIgnoreCase(raw) ? null : stripQuotes(raw);

        int index = key.ordinal();
        Object before = vals()[index];
        int sizeBefore = size();
        putRaw(key, value);
        Object after = vals()[index];
        int sizeAfter = size();

        System.out.printf("put(%s, %s)%n", qualified(key), quoted(value));
        String sizeText = before == null
                ? String.format("size: %d -> %d", sizeBefore, sizeAfter)
                : String.format("size unchanged (%d)", sizeAfter);
        System.out.printf("  vals[%d]: %s -> %s   %s%n",
                index, slotText(before), slotText(after), sizeText);
        if (after == nullSentinel()) {
            System.out.println("  note: maskNull stored the NULL sentinel; containsKey stays true");
        }
        show();
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private void putRaw(Enum<?> key, String value) {
        ((EnumMap) map).put(key, value);
    }

    private void doGet(String rest) {
        if (!requireMap()) {
            return;
        }
        Enum<?> key = findKey(rest);
        if (key == null) {
            return;
        }
        Object slot = vals()[key.ordinal()];
        String result = map.get(key);
        System.out.printf("get(%s) -> %s", qualified(key), quoted(result));
        if (slot == null) {
            System.out.print("   (vals[" + key.ordinal() + "] == null: no mapping)");
        } else if (slot == nullSentinel()) {
            System.out.print("   (unmaskNull(NULL): looks absent, but containsKey is true)");
        }
        System.out.println();
    }

    private void doContainsKey(String rest) {
        if (!requireMap()) {
            return;
        }
        Enum<?> key = findKey(rest);
        if (key == null) {
            return;
        }
        int index = key.ordinal();
        Object slot = vals()[index];
        boolean present = map.containsKey(key);
        String why = slot == null
                ? "vals[" + index + "] == null: no mapping"
                : "vals[" + index + "] != null: present, even if it is the NULL sentinel";
        System.out.printf("containsKey(%s) -> %s   (%s)%n", qualified(key), present, why);
    }

    private void doRemove(String rest) {
        if (!requireMap()) {
            return;
        }
        Enum<?> key = findKey(rest);
        if (key == null) {
            return;
        }
        int index = key.ordinal();
        Object before = vals()[index];
        int sizeBefore = size();
        String removed = map.remove(key);
        int sizeAfter = size();

        System.out.printf("remove(%s) -> %s%n", qualified(key), quoted(removed));
        if (before == null) {
            System.out.printf("  vals[%d] was already null: no-op, size unchanged (%d)%n", index, sizeAfter);
        } else {
            System.out.printf("  vals[%d]: %s -> null   size: %d -> %d%n",
                    index, slotText(before), sizeBefore, sizeAfter);
            if (before == nullSentinel()) {
                System.out.println("  note: the slot held the NULL sentinel, so remove() returned null (unmaskNull)");
            }
        }
        show();
    }

    private void doClear() {
        if (!requireMap()) {
            return;
        }
        int sizeBefore = size();
        map.clear();
        System.out.printf("clear()   Arrays.fill(vals, null); size: %d -> %d%n", sizeBefore, size());
        show();
    }

    private void doReset() {
        map = null;
        System.out.println("map dropped (keyType, keyUniverse, vals, size are gone): run new <enum>");
    }

    // ---- state output ----

    private void show() {
        if (map == null) {
            System.out.println("no map yet: run new <enum> first");
            return;
        }
        Object[] universe = keyUniverse();
        Object[] values = vals();
        int keyWidth = 0;
        for (Object constant : universe) {
            keyWidth = Math.max(keyWidth, ((Enum<?>) constant).name().length());
        }
        int indexWidth = String.valueOf(values.length - 1).length();
        System.out.printf("  EnumMap<%s, String>   keyType = %s.class%n", typeName(), typeName());
        System.out.printf("  keyUniverse.length = %d   vals.length = %d   size = %d%n",
                universe.length, values.length, size());
        for (int i = 0; i < values.length; i++) {
            System.out.printf("    [%" + indexWidth + "d] %s -> %s%n",
                    i, pad(((Enum<?>) universe[i]).name(), keyWidth), slotText(values[i]));
        }
        System.out.println();
    }

    /** null = no mapping, NULL sentinel = mapped to null, otherwise the quoted value. */
    private String slotText(Object slot) {
        if (slot == null) {
            return "null";
        }
        if (slot == nullSentinel()) {
            return "NULL sentinel (mapped to null)";
        }
        return "\"" + slot + "\"";
    }

    // ---- reflection accessors ----

    private Object[] keyUniverse() {
        return readArray(keyUniverseField);
    }

    private Object[] vals() {
        return readArray(valsField);
    }

    private Object[] readArray(Field field) {
        try {
            return (Object[]) field.get(map);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    private Class<?> keyType() {
        try {
            return (Class<?>) keyTypeField.get(map);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    private int size() {
        try {
            return sizeField.getInt(map);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    private Object nullSentinel() {
        try {
            return nullField.get(null);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    private String typeName() {
        return keyType().getSimpleName();
    }

    // ---- helpers ----

    private boolean requireMap() {
        if (map == null) {
            System.out.println("no map yet: run new <enum> first");
            return false;
        }
        return true;
    }

    private EnumType findType(String arg) {
        for (EnumType t : TYPES) {
            if (t.displayName().equalsIgnoreCase(arg) || t.displayName().equals(arg)) {
                return t;
            }
        }
        return null;
    }

    private Enum<?> findKey(String name) {
        if (map == null) {
            return null;
        }
        for (Object constant : keyUniverse()) {
            Enum<?> e = (Enum<?>) constant;
            if (e.name().equals(name) || e.name().equalsIgnoreCase(name)) {
                return e;
            }
        }
        System.out.println("unknown key: " + name + " (valid: " + keyNames() + ")");
        return null;
    }

    private String keyNames() {
        StringBuilder builder = new StringBuilder();
        for (Object constant : keyUniverse()) {
            if (!builder.isEmpty()) {
                builder.append(' ');
            }
            builder.append(((Enum<?>) constant).name());
        }
        return builder.toString();
    }

    private String qualified(Enum<?> key) {
        return typeName() + "." + key.name();
    }

    private static String quoted(String value) {
        return value == null ? "null" : "\"" + value + "\"";
    }

    private static String stripQuotes(String value) {
        if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
            return value.substring(1, value.length() - 1);
        }
        return value;
    }

    /** Right-pads with spaces so that keys line up in the slot listing. */
    private static String pad(String s, int width) {
        return s + " ".repeat(width - s.length());
    }
}

参考资料

相关推荐
秋天的怀念_David2 小时前
第二十二夜 · 数字迷雾
java·程序人生·算法
Raas1003 小时前
AI网关部署方案?从部署到上线全流程
java·开发语言·人工智能·gateway·ai网关·企业级ai网关·企业级ai产品
niucloud-admin3 小时前
NIUCLOUD PHP SAAS上门家政使用文档:城市管理
java·前端·php
霸道流氓气质4 小时前
智能客服组件:分层意图识别、RAG Advisor 与人工转接状态机的 Vue3 + Spring AI 生产级实战
java·人工智能·spring
captain3764 小时前
MyBatis操作数据库(入门2)
java·数据库·spring boot·后端·spring·mybatis
心中有你02145 小时前
Java实现Dijkstra最短路径算法(邻接表 + 优先队列)
java·数据库·算法
步行cgn5 小时前
Spring MVC + Thymeleaf 模板解析失败:从 500 错误到页面正常显示
java·spring·mvc
李剑一5 小时前
没收用户的钱冲史上最大IPO?2 万亿美元的估值有多少是罚没来的
aigc·openai·ai编程
lgcgkCQ6 小时前
【AI 转型实战 1】SpringAI 入门:搭建基础大模型对话能力
java·ai·agent·rag·spring ai·mcp·langchain4j