JDK 21 中 List.of 的实现细节:从 List12 到 ListN

引子:List.of 返回的到底是什么?

大家日常写 Java 代码时,一个创建小列表的常见方式如下 ⬇️

java 复制代码
List<Integer> list = List.of(1, 2, 3);

它看起来平平无奇,但有几个问题 ⬇️

  1. 它返回的是 ArrayList 吗?
  2. 为什么 get(i) 是 O(1)\mathcal{O}(1) O(1) 的,底层是数组还是链表?
  3. List.of(1) 和 List.of(1, 2) 的内部结构一样吗?
  4. 为什么同样是越界访问,List.of(1).get(1) 抛 IndexOutOfBoundsException,而 List.of(1, 2, 3).get(3) 抛 ArrayIndexOutOfBoundsException?

我们先给出结论,再逐条验证。本文基于 OpenJDK 21,源码引用自 JDK 21 的 java.util.List 与 java.util.ImmutableCollections。

一张表看完分派规则

List.of 有 12 种重载形式(0~10 个参数的固定重载,再加一个可变参数重载)。在 java.util.List 我们可以找到以下方法 ⬇️

java 复制代码
static <E> List<E> of();
static <E> List<E> of(E e1);
static <E> List<E> of(E e1, E e2);
static <E> List<E> of(E e1, E e2, E e3);
static <E> List<E> of(E e1, E e2, E e3, E e4);
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5);
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6);
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7);
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8);
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9);
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10);
static <E> List<E> of(E... elements);

参数的 个数 决定实现类 ⬇️

参数个数 nn n 实现类 底层存储 元素数组的分配
n=0n=0 n=0 ImmutableCollections$ListN 共享的 EMPTY_LIST 类初始化时创建一次,之后复用
n=1n=1 n=1 ImmutableCollections$List12 两个 final 字段:e0、e1 = EMPTY 哨兵 0 次
n=2n=2 n=2 ImmutableCollections$List12 两个 final 字段:e0、e1 0 次
3≤n≤103 \le n\le 10 3≤n≤10 ImmutableCollections$ListN Object[n],长度恰好 n 1 次(复用调用点的数组,不再复制)
n>10n\gt 10 n>10 ImmutableCollections$ListN Object[n],长度恰好 n 额外复制 1 份(先复制再查 null)

一句话概括:元素不超过 2 个时用两个字段,超过 2 个才创建数组;空列表则是一个全局共享的实例。

怎么看到这些私有字段

通过反射就能访问私有字段 ⬇️ (请将以下代码保存为 ListProbe.java)

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

public class ListProbe {
    public static void main(String[] args) throws Exception {
        List<List<Integer>> lists = new ArrayList<>();
        lists.add(List.of());
        lists.add(List.of(42));
        lists.add(List.of(1, 2));
        lists.add(List.of(1, 2, 3));
        lists.add(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
        lists.add(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11));

        for (List<Integer> list : lists) {
            Class<?> c = list.getClass();
            List<String> fields = new ArrayList<>();
            for (Field f : c.getDeclaredFields()) {
                fields.add(f.getType().getSimpleName() + " " + f.getName());
            }
            StringBuilder builder = new StringBuilder();
            builder.append(String.format("size=%-2d  %-44s [%s]", list.size(), c.getName(),
                    String.join(", ", fields)));
            if (list.size() >= 3) {
                Field f = c.getDeclaredField("elements");
                f.setAccessible(true);
                builder.append("  elements.length=").append(((Object[]) f.get(list)).length);
            }
            System.out.println(builder);
        }
    }
}

从 JDK 9 开始,反射访问 java.base 的私有字段需要显式开放包 ⬇️

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

在我本地(OpenJDK 21)输出如下 ⬇️

text 复制代码
size=0   java.util.ImmutableCollections$ListN         [Object[] elements, boolean allowNulls]
size=1   java.util.ImmutableCollections$List12        [Object e0, Object e1]
size=2   java.util.ImmutableCollections$List12        [Object e0, Object e1]
size=3   java.util.ImmutableCollections$ListN         [Object[] elements, boolean allowNulls]  elements.length=3
size=10  java.util.ImmutableCollections$ListN         [Object[] elements, boolean allowNulls]  elements.length=10
size=11  java.util.ImmutableCollections$ListN         [Object[] elements, boolean allowNulls]  elements.length=11

如果忘了加 --add-opens,setAccessible(true) 会抛 java.lang.reflect.InaccessibleObjectException ⬇️ 这也是本文配套 GUI 程序启动时先做预检的原因。

1 个和 2 个元素:List12 用两个字段撑起整个列表

List12 的源码如下 ⬇️ (本文不关心的方法,其代码都用 ... 表示)

java 复制代码
@jdk.internal.ValueBased
static final class List12<E> extends AbstractImmutableList<E>
        implements Serializable {

    @Stable
    private final E e0;

    @Stable
    private final Object e1;

    List12(E e0) {
        this.e0 = Objects.requireNonNull(e0);
        // Use EMPTY as a sentinel for an unused element: not using null
        // enables constant folding optimizations over single-element lists
        this.e1 = EMPTY;
    }

    List12(E e0, E e1) {
        this.e0 = Objects.requireNonNull(e0);
        this.e1 = Objects.requireNonNull(e1);
    }

    @Override
    public int size() {
        return e1 != EMPTY ? 2 : 1;
    }

    @Override
    public boolean isEmpty() {
        return false;
    }

    @Override
    @SuppressWarnings("unchecked")
    public E get(int index) {
        if (index == 0) {
            return e0;
        } else if (index == 1 && e1 != EMPTY) {
            return (E)e1;
        }
        throw outOfBounds(index);
    }

    @Override
    public int indexOf(Object o) {
        Objects.requireNonNull(o);
        if (o.equals(e0)) {
            return 0;
        } else if (e1 != EMPTY && o.equals(e1)) {
            return 1;
        } else {
            return -1;
        }
    }

    @Override
    public int lastIndexOf(Object o) {
        Objects.requireNonNull(o);
        if (e1 != EMPTY && o.equals(e1)) {
            return 1;
        } else if (o.equals(e0)) {
            return 0;
        } else {
            return -1;
        }
    }

    @java.io.Serial
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        ...
    }

    @java.io.Serial
    private Object writeReplace() {
        ...
    }

    @Override
    public Object[] toArray() {
        ...
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        ...
    }
}

几个值得注意的点 ⬇️

  1. 没有 size 字段 :size() 靠 e1 != EMPTY 判断:存了哨兵就是 1 个元素,否则是 2 个。
  2. e1 声明为 Object 而不是 E :因为它要装哨兵 EMPTY(一个普通的 new Object()),不能保证是 E 类型。
  3. 哨兵不用 null:源码注释写明了原因, enables constant folding optimizations over single-element lists
  4. get :get(0) 读 e0,get(1) 才读 e1;indexOf 也是直接比较两个字段,最多 2 次 equals,没有循环。

越界时抛出的异常来自父类的方法 ⬇️

java 复制代码
IndexOutOfBoundsException outOfBounds(int index) {
    return new IndexOutOfBoundsException("Index: " + index + " Size: " + size());
}

实测结果示例 ⬇️

text 复制代码
List.of(7).get(1)        -> java.lang.IndexOutOfBoundsException: Index: 1 Size: 1
List.of(7, 8).get(2)     -> java.lang.IndexOutOfBoundsException: Index: 2 Size: 2

3 个及以上:ListN 就是一个长度恰好为 n 的数组

ListN 的源码如下 ⬇️ (本文不关心的方法,其代码都用 ... 表示)

java 复制代码
@jdk.internal.ValueBased
static final class ListN<E> extends AbstractImmutableList<E>
        implements Serializable {

    @Stable
    private final E[] elements;

    @Stable
    private final boolean allowNulls;

    // caller must ensure that elements has no nulls if allowNulls is false
    private ListN(E[] elements, boolean allowNulls) {
        this.elements = elements;
        this.allowNulls = allowNulls;
    }

    @Override
    public boolean isEmpty() {
        return elements.length == 0;
    }

    @Override
    public int size() {
        return elements.length;
    }

    @Override
    public E get(int index) {
        return elements[index];
    }

    @java.io.Serial
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        ...
    }

    @java.io.Serial
    private Object writeReplace() {
        ...
    }

    @Override
    public Object[] toArray() {
        ...
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        ...
    }

    @Override
    public int indexOf(Object o) {
        if (!allowNulls && o == null) {
            throw new NullPointerException();
        }
        Object[] es = elements;
        for (int i = 0; i < es.length; i++) {
            if (Objects.equals(o, es[i])) {
                return i;
            }
        }
        return -1;
    }

    @Override
    public int lastIndexOf(Object o) {
        if (!allowNulls && o == null) {
            throw new NullPointerException();
        }
        Object[] es = elements;
        for (int i = es.length - 1; i >= 0; i--) {
            if (Objects.equals(o, es[i])) {
                return i;
            }
        }
        return -1;
    }
}

对比 List12,差别很直接 ⬇️

  1. 数组长度恰好等于元素个数,没有为了扩容预留的空位。
  2. size() 就是 elements.length ,get(i) 就是 elements[i],所以时间复杂度是 O(1)\mathcal{O}(1) O(1)。
  3. 构造器本身不复制、也不检查 null ,注释写明"调用方必须保证没有 null"。null 检查发生在构造之前的工厂方法里(见下一节)。
  4. indexOf 是线性扫描 ,从下标 0 开始用 Objects.equals 逐个比较,第一个匹配就返回。
  5. allowNulls 可供 Stream.toList() 这类内部路径使用;List.of 传的是 false,所以 List.of(...) 一定拒绝 null。

因为 get 直接把下标交给数组,越界异常由 JVM 抛出 ⬇️

text 复制代码
List.of(1, 2, 3).get(3)  -> java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
List.of(1, 2, 3).get(-1) -> java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 3
List.of().get(0)         -> java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0

为什么 3~10 个参数和 11 个以上不一样

List.of 里 3~10 个参数的固定重载统一走 listFromTrustedArray (以 3 个入参为例) ⬇️

java 复制代码
static <E> List<E> of(E e1, E e2, E e3) {
    return ImmutableCollections.listFromTrustedArray(e1, e2, e3);
}

而 listFromTrustedArray 会把收到的 Object[] 直接当作内部存储 ⬇️

java 复制代码
@SuppressWarnings("unchecked")
static <E> List<E> listFromTrustedArray(Object... input) {
    assert input.getClass() == Object[].class;
    for (Object o : input) { // implicit null check of 'input' array
        Objects.requireNonNull(o);
    }

    return switch (input.length) {
        case 0  -> (List<E>) ImmutableCollections.EMPTY_LIST;
        case 1  -> (List<E>) new List12<>(input[0]);
        case 2  -> (List<E>) new List12<>(input[0], input[1]);
        default -> (List<E>) new ListN<>(input, false);
    };
}

11 个及以上参数会落到可变参数重载,走另一条路 ⬇️

java 复制代码
@SafeVarargs
@SuppressWarnings("varargs")
static <E> List<E> of(E... elements) {
    switch (elements.length) { // implicit null check of elements
        case 0:
            @SuppressWarnings("unchecked")
            var list = (List<E>) ImmutableCollections.EMPTY_LIST;
            return list;
        case 1:
            return new ImmutableCollections.List12<>(elements[0]);
        case 2:
            return new ImmutableCollections.List12<>(elements[0], elements[1]);
        default:
            return ImmutableCollections.listFromArray(elements);
    }
}

listFromArray 面对的是调用方可能仍然持有的数组,所以必须先复制、再逐个检查 null ⬇️

java 复制代码
static <E> List<E> listFromArray(E... input) {
    // copy and check manually to avoid TOCTOU
    E[] tmp = (E[])new Object[input.length]; // implicit nullcheck of input
    for (int i = 0; i < input.length; i++) {
        tmp[i] = Objects.requireNonNull(input[i]);
    }
    return new ListN<>(tmp, false);
}

注释里的 TOCTOU 指的是"检查完之后、复制之前数组被改掉"的时间窗口。先复制一份私有的 tmp,后续检查针对副本,就不会有这个问题。

这个差异可以直接观察到 ⬇️

java 复制代码
Integer[] src = {1, 2, 3};
List<Integer> list = List.of(src); // 走可变参数重载,内部复制了一份
src[0] = 99;
System.out.println(list.get(0));   // 1,原数组的修改影响不到列表

0 个元素:全局共享的 EMPTY_LIST

List.of() 不新建对象,直接返回 ImmutableCollections.EMPTY_LIST ⬇️

java 复制代码
static {
    CDS.initializeFromArchive(ImmutableCollections.class);
    if (archivedObjects == null) {
        EMPTY = new Object();
        EMPTY_LIST = new ListN<>(new Object[0], false);
        // ...
        archivedObjects = new Object[] { EMPTY, EMPTY_LIST, ... };
    } else {
        EMPTY = archivedObjects[0];
        EMPTY_LIST = (ListN)archivedObjects[1];
        // ...
    }
}

由此可以推出两个结论 ⬇️

  1. List.of() 返回的不是 List12,而是 ListN(elements 长度为 0)
  2. 调用 List.of() 得到的是同一个对象,那么 List.of() == List.of() 为 true

关于 CDS,我问了问 trae (完整的回答有点长,这里直截取了开头的部分)⬇️ 有兴趣的读者可以自己探索一下(作者水平有限,本文中就不讨论这个话题了)

"不可变"是怎么做到的

不可变不是靠约定,而是靠父类把所有修改方法都覆写成抛异常。AbstractImmutableCollection 负责 Collection 层面的方法 ⬇️

java 复制代码
abstract static class AbstractImmutableCollection<E> extends AbstractCollection<E> {
    // all mutating methods throw UnsupportedOperationException
    @Override public boolean add(E e) { throw uoe(); }
    @Override public boolean addAll(Collection<? extends E> c) { throw uoe(); }
    @Override public void    clear() { throw uoe(); }
    @Override public boolean remove(Object o) { throw uoe(); }
    @Override public boolean removeAll(Collection<?> c) { throw uoe(); }
    @Override public boolean removeIf(Predicate<? super E> filter) { throw uoe(); }
    @Override public boolean retainAll(Collection<?> c) { throw uoe(); }
}

AbstractImmutableList 再补上 List 专属的修改方法 ⬇️

java 复制代码
// all mutating methods throw UnsupportedOperationException
@Override public void    add(int index, E element) { throw uoe(); }
@Override public boolean addAll(int index, Collection<? extends E> c) { throw uoe(); }
@Override public E       remove(int index) { throw uoe(); }
@Override public void    replaceAll(UnaryOperator<E> operator) { throw uoe(); }
@Override public E       set(int index, E element) { throw uoe(); }
@Override public void    sort(Comparator<? super E> c) { throw uoe(); }

异常工厂方法本身很朴素 ⬇️

java 复制代码
static UnsupportedOperationException uoe() { return new UnsupportedOperationException(); }

注意它不带 message。所以实测日志里看到的是 ⬇️

text 复制代码
add(1)     -> UnsupportedOperationException (no message)
remove(1)  -> UnsupportedOperationException (no message)
clear()    -> UnsupportedOperationException (no message)

同样被拦住的还有视图和迭代器:subList(...) 得到的是一个只读视图,subList.clear() 抛 UOE;iterator().remove()、listIterator().set(...) 也都抛 UOE。

为什么越界异常类型不一样

两段源码已经把原因写清楚了 ⬇️

  • List12.get 自己判断下标,抛 outOfBounds(index) → IndexOutOfBoundsException,消息是 Index: n Size: size。
  • ListN.get 直接 return elements[index],边界检查交给 JVM → ArrayIndexOutOfBoundsException,消息是 Index n out of bounds for length len。

这一点在写代码时有个实际结论:ArrayIndexOutOfBoundsException 是 IndexOutOfBoundsException 的子类,所以 ⬇️

java 复制代码
try {
    list.get(index);
} catch (IndexOutOfBoundsException e) {   // 两种实现都能接住
    // ...
}

而如果你只 catch (ArrayIndexOutOfBoundsException e),在单元素或双元素列表上就会出现漏过的情况。异常的具体类型是实现细节,不要把它当成 API 语义的一部分。

indexOf:线性扫描,但 List12 会短路

ListN 的逻辑是标准线性扫描:从下标 0 开始,用 Objects.equals 逐个比较,首个匹配即返回,最坏情况要比较 n 次。

List12 没有循环,直接比较 e0 和 e1,最多 2 次 equals,所以小列表的查找反而更直接。

顺带一个容易踩的点:indexOf(null) 会抛 NullPointerException,而不是返回 -1。List12 里是 Objects.requireNonNull(o);ListN 里是显式判断 if (!allowNulls && o == null) throw new NullPointerException()。List.of 构造出来的列表 allowNulls 恒为 false,所以两者行为一致。

实测(元素为 [1] 的列表)⬇️

text 复制代码
indexOf(1)  -> 0  (compared 1 of 1)
indexOf(-1) -> -1 (not found)

和常见列表的对照

创建方式 可变性 允许 null 与源数组的关系
List.of(...) 完全不可变 否,构造时抛 NPE 自己持有副本或独立数组,源数组改动不影响
Arrays.asList(array) 固定长度,可 set,不可增删 是 是数组的视图,改原数组会影响列表
new ArrayList<>(List.of(...)) 可变 取决于源 独立副本
List.copyOf(coll) 完全不可变 否 入参已是不可变列表时直接复用同一实例

List.copyOf 最后一条来自源码 ⬇️

java 复制代码
static <E> List<E> listCopy(Collection<? extends E> coll) {
    if (coll instanceof List12 || (coll instanceof ListN<?> c && !c.allowNulls)) {
        return (List<E>)coll;
    } else if (coll.isEmpty()) { // implicit nullcheck of coll
        return List.of();
    } else {
        return (List<E>)List.of(coll.toArray());
    }
}

所以 List.copyOf(list) == list 对 List.of 的结果成立(实测为 true),这也是"不可变对象可以安全共享"的一个具体体现。

配套的 GUI 演示程序

上面的结论可以在配套的 Swing 程序里手动复现:ListOfBehaviorDemo.java。它通过反射读取真实对象的私有字段,把"类名 / 字段 / 数组长度 / 越界异常类型"直接画在界面上。

运行方式 ⬇️

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

界面分三块 ⬇️

  • demo setup :n = 下拉(0、1、2、3、4、5、8、12)+ rule = 下拉(连续自然数 / 连续偶数 / 连续奇数),下方实时显示将要执行的 List.of(...) 调用和预测的实现类。
  • factory method :一个按钮,文本就是即将执行的调用,例如 List.of(0, 2, 4, 6)。
  • index API / element API :get(i)(下拉里额外提供一项越界下标)、indexOf(v)、add(v)、remove(v)、clear(),用来观察越界异常和 UOE。

成功创建列表后,参数区和工厂按钮会锁定(一次演示只建一个列表),点 Reset 后解锁。右侧上半部分是结构图:List12 画成 e0/e1 两个格子(单元素时 e1 显示为哨兵),ListN 画成下标数组;下半部分是字段形状、运行时信息与操作日志。

n = 1,List12 的 e1 显示为 <EMPTY> 哨兵 ⬇️

n = 4,ListN 的 elements 长度恰好为 4 ⬇️

n = 0,List.of() 命中共享的 EMPTY_LIST ⬇️

越界 get 会触发异常提示 ⬇️

小结

把上面的结论收成一张清单 ⬇️

  1. List.of 的实现类是 ImmutableCollections$List12 或 ListN,不是 ArrayList。
  2. 0 个元素 → 全局共享的 EMPTY_LIST(本身是 ListN,Object[0])。
  3. 1 个元素 → List12,e1 存 EMPTY 哨兵,size() 不需要计数字段。
  4. 2 个元素 → List12,两个 final 字段,没有数组。
  5. 3 个及以上 → ListN,Object[n],长度恰好为 n。
  6. 3~10 个参数复用调用点的数组;11 个及以上会多复制一份并逐个查 null。
  7. get(i) 是 O(1);indexOf 是线性扫描(List12 最多比较 2 次)。
  8. 构造时拒绝 null;indexOf(null) 也抛 NPE。
  9. 所有修改方法都抛 UnsupportedOperationException(无 message)。
  10. 越界异常:List12 抛 IndexOutOfBoundsException,ListN 抛 ArrayIndexOutOfBoundsException,后者是前者的子类。

参考资料 ⬇️

  • OpenJDK 21 源码:java.base/java/util/ImmutableCollections.java
  • OpenJDK 21 源码:java.base/java/util/List.java

完整代码

java 复制代码
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.function.IntFunction;
import java.util.function.IntUnaryOperator;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.border.Border;
import javax.swing.border.TitledBorder;

/**
 * Swing demo of the internals of java.util.List.of(...) on JDK 21. Dispatch on
 * argument count: 0 -> shared EMPTY_LIST (ListN, Object[0]); 1 -> List12(e0)
 * with e1 = EMPTY sentinel; 2 -> List12(e0, e1); N >= 3 -> ListN, elements =
 * Object[N] (exactly N, no padding). A list is an array or two fields, not a
 * hash table: get(i) is elements[i] (List12 branches on i), indexOf walks from
 * 0 with equals, duplicates are allowed, nulls throw NPE, add/remove/clear
 * throw UnsupportedOperationException, and iteration order is insertion order
 * on every run (no SALT randomization). Out-of-range get differs by class:
 * List12 throws IndexOutOfBoundsException, ListN ArrayIndexOutOfBoundsException.
 * <p>
 * Fields are private, so reflection is required. Run with:
 * javac ListOfBehaviorDemo.java
 * java --add-opens=java.base/java.util=ALL-UNNAMED ListOfBehaviorDemo
 */
public class ListOfBehaviorDemo {

    // ---- selectable parameters: list size x filling rule ----

    /**
     * One filling rule: given n it produces the List.of argument list. The
     * displayed call text comes from that same Integer[], so it cannot
     * disagree with the created list.
     */
    private record Rule(String label, String note, IntFunction<Integer[]> gen) {
        @Override
        public String toString() {
            return label;
        }
    }

    /** One entry of the "element" combo box: a member, or an absent probe. */
    private record ElementChoice(String label, Integer value) {
        @Override
        public String toString() {
            return label;
        }
    }

    /** One entry of the "index" combo box: a valid index, or one past the end. */
    private record IndexChoice(String label, int value) {
        @Override
        public String toString() {
            return label;
        }
    }

    private static final List<Rule> RULES = List.of(
            new Rule("consecutive 1..N", "index == value, easy to follow",
                    n -> seq(n, i -> i + 1)),
            new Rule("evens 2i", "values differ from indices, order kept",
                    n -> seq(n, i -> 2 * i)),
            new Rule("odds 2i+1", "same shape as evens, shifted by one",
                    n -> seq(n, i -> 2 * i + 1)));

    /** Sizes offered by the "n =" combo; n <= 2 still yields List12. */
    private static final List<Integer> SIZES = List.of(0, 1, 2, 3, 4, 5, 8, 12);

    /** The absent probe appended to the element combo box; all rules yield non-negative values. */
    private static final Integer ABSENT_VALUE = -1;

    /** n boxed values built by f(i); every rule with a closed form goes through here. */
    private static Integer[] seq(int n, IntUnaryOperator f) {
        Integer[] v = new Integer[n];
        for (int i = 0; i < n; i++) {
            v[i] = f.applyAsInt(i);
        }
        return v;
    }

    // ---- shared design language with SetOfBehaviorDemo ----
    /** Blue for buttons that map 1:1 to Java code (IDE syntax-highlight blue). */
    private static final Color CODE_COLOR = new Color(21, 101, 192);
    /** Dark gray for demo helper buttons: clearly secondary, not disabled. */
    private static final Color DEMO_COLOR = new Color(93, 93, 93);
    /** Light gray used when a button is disabled. */
    private static final Color DISABLED_COLOR = new Color(175, 175, 175);

    private static final Color SLOT_FILLED_COLOR = new Color(66, 133, 244);
    private static final Color SENTINEL_BG = new Color(255, 248, 225);
    private static final Color SENTINEL_FG = new Color(158, 101, 0);
    private static final Color LAST_TOUCHED_COLOR = new Color(244, 180, 0);

    /** Error feedback: light red banner background, dark red text/border. */
    private static final Color ERROR_BG = new Color(253, 237, 237);
    private static final Color ERROR_FG = new Color(183, 28, 28);
    private static final Color ERROR_LINE = new Color(239, 83, 80);

    /** Red-outlined variant of the plain etched box, used for the throwing input. */
    private static final Border ERROR_BOX = BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(ERROR_LINE, 2),
            BorderFactory.createEmptyBorder(2, 2, 2, 2));
    private static final Border PLAIN_BOX = BorderFactory.createEtchedBorder();

    private static final Font CODE_FONT = new Font(Font.MONOSPACED, Font.BOLD, 13);

    // ---- state ----
    /** null means "no list yet": element operations stay disabled until List.of runs. */
    private List<Integer> list;
    /** Index highlighted as the result of the most recent successful get / find. */
    private int lastTouchedSlot = -1;
    /** Indices compared by the most recent indexOf, in visiting order. */
    private List<Integer> probePath = List.of();
    /** Everything the view needs about the current object; grid and text panel share it. */
    private List<Slot> slots = List.of();
    private boolean slotsFromList12;
    private boolean emptySharedList;

    /** One drawn cell: a ListN array slot, or a List12 field (e0 / e1). */
    private record Slot(int index, String title, String value, String note,
                        boolean sentinel) {
    }

    // ---- UI ----
    private JButton createButton;
    private JButton getButton;
    private JButton indexOfButton;
    private JButton addButton;
    private JButton removeButton;
    private JButton clearButton;
    private JButton resetButton;
    private JComboBox<Integer> sizeCombo;
    private JComboBox<Rule> ruleCombo;
    private JComboBox<IndexChoice> indexCombo;
    private JComboBox<ElementChoice> elementCombo;
    private JLabel glossLabel;
    private JLabel errorBanner;
    /** The size + rule selector box that turns red when the call throws. */
    private JPanel presetGroupPanel;
    /** Set when JDK 9+ module rules deny reflection: the banner then stays up. */
    private boolean reflectionBlind;

    private JLabel runtimeClassLabel;
    private JLabel sizeLabel;
    private JLabel storageLabel;
    private JLabel accessLabel;
    private JLabel iterationLabel;
    private JLabel toStringLabel;

    private JPanel statePanel;
    private JTextArea fieldShapeArea;
    private JTextArea logArea;
    private SlotGrid slotGrid;

    public static void main(String[] args) {
        SwingUtilities.invokeLater(ListOfBehaviorDemo::new);
    }

    private ListOfBehaviorDemo() {
        createAndShowGui();
    }

    private void createAndShowGui() {
        // Snappier, longer-lived tooltips: the defaults (750 ms to appear,
        // 4 s auto-dismiss) make a mapping table this long feel unreliable.
        ToolTipManager.sharedInstance().setInitialDelay(300);
        ToolTipManager.sharedInstance().setDismissDelay(12000);

        JFrame frame = new JFrame("List.of internals demo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLayout(new BorderLayout());

        JPanel control = new JPanel();
        control.setLayout(new BoxLayout(control, BoxLayout.Y_AXIS));
        control.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));

        // ---------- row 0: which List.of call to demo ----------
        sizeCombo = new JComboBox<>();
        for (Integer n : SIZES) {
            sizeCombo.addItem(n);
        }
        sizeCombo.setSelectedItem(4);
        sizeCombo.setToolTipText("Number of arguments passed to List.of(...)");

        ruleCombo = new JComboBox<>();
        for (Rule r : RULES) {
            ruleCombo.addItem(r);
        }
        ruleCombo.setToolTipText("How the Integer arguments are chosen");
        glossLabel = new JLabel();

        presetGroupPanel = new JPanel();
        presetGroupPanel.setBorder(BorderFactory.createEtchedBorder());
        presetGroupPanel.add(new JLabel("n ="));
        presetGroupPanel.add(sizeCombo);
        presetGroupPanel.add(new JLabel("rule ="));
        presetGroupPanel.add(ruleCombo);
        presetGroupPanel.add(glossLabel);

        control.add(row("demo setup", DEMO_COLOR, presetGroupPanel));

        // ---------- row 1: the factory method ----------
        createButton = new JButton();
        createButton.setFont(CODE_FONT);
        control.add(row("factory method", CODE_COLOR, createButton));

        // ---------- row 2: index-based access ----------
        getButton = new JButton();
        getButton.setFont(CODE_FONT);
        indexCombo = new JComboBox<>();
        indexCombo.setToolTipText("Index passed to get(int)");
        indexCombo.addActionListener(e -> updateOpButtonText());

        JPanel indexGroup = new JPanel();
        indexGroup.setBorder(BorderFactory.createEtchedBorder());
        indexGroup.add(new JLabel("index ="));
        indexGroup.add(indexCombo);

        control.add(row("index API", CODE_COLOR, indexGroup, getButton));

        // ---------- row 3: element-based operations ----------
        indexOfButton = new JButton();
        addButton = new JButton();
        removeButton = new JButton();
        clearButton = new JButton("clear()");
        for (JButton b : new JButton[]{indexOfButton, addButton, removeButton, clearButton}) {
            b.setFont(CODE_FONT);
        }
        clearButton.setToolTipText("Runs: list.clear()  (immutable lists always throw)");

        elementCombo = new JComboBox<>();
        elementCombo.setToolTipText("Picks the argument for indexOf / add / remove");
        elementCombo.addActionListener(e -> updateOpButtonText());

        JPanel selGroup = new JPanel();
        selGroup.setBorder(BorderFactory.createEtchedBorder());
        selGroup.add(new JLabel("element ="));
        selGroup.add(elementCombo);

        control.add(row("element API", CODE_COLOR, selGroup, indexOfButton, addButton,
                removeButton, clearButton));

        // ---------- row 4: demo-only helpers ----------
        resetButton = new JButton("Reset");
        resetButton.setToolTipText("Demo control, not a Java API call: returns to the initial state");
        control.add(row("demo only", DEMO_COLOR, resetButton));

        // ---------- error banner: hidden until a call throws ----------
        errorBanner = new JLabel();
        errorBanner.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
        errorBanner.setForeground(ERROR_FG);
        errorBanner.setOpaque(true);
        errorBanner.setBackground(ERROR_BG);
        errorBanner.setBorder(BorderFactory.createCompoundBorder(
                BorderFactory.createLineBorder(ERROR_LINE),
                BorderFactory.createEmptyBorder(4, 8, 4, 8)));
        errorBanner.setVisible(false);
        JPanel bannerRow = new JPanel(new BorderLayout());
        bannerRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        bannerRow.add(errorBanner, BorderLayout.CENTER);
        control.add(bannerRow);

        // ---------- state panel: one labeled line per internal fact ----------
        statePanel = new JPanel();
        statePanel.setLayout(new BoxLayout(statePanel, BoxLayout.Y_AXIS));
        statePanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        statePanel.setBorder(new TitledBorder("internal state of the returned list"));

        runtimeClassLabel = new JLabel();
        sizeLabel = new JLabel();
        storageLabel = new JLabel();
        accessLabel = new JLabel();
        iterationLabel = new JLabel();
        toStringLabel = new JLabel();
        for (JLabel line : new JLabel[]{runtimeClassLabel, sizeLabel, storageLabel,
                accessLabel, iterationLabel, toStringLabel}) {
            line.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
            JPanel row = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 2));
            row.setAlignmentX(Component.LEFT_ALIGNMENT);
            row.add(line);
            statePanel.add(row);
        }

        slotGrid = new SlotGrid();
        statePanel.add(slotGrid);

        // ---------- field shape panel ----------
        // Prints only what the current object actually holds, read via reflection.
        fieldShapeArea = new JTextArea();
        fieldShapeArea.setEditable(false);
        fieldShapeArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
        JScrollPane fieldScroll = new JScrollPane(fieldShapeArea);
        fieldScroll.setBorder(new TitledBorder("fields (read from the actual object)"));

        // Side by side: the live slots on the left, the textual explanation on
        // the right. A split pane instead of BorderLayout: with WEST + CENTER,
        // the text panel silently absorbed every width deficit and could
        // collapse to a sliver whenever the window got narrower than the
        // fixed-width slot grid. resizeWeight keeps both sides usable.
        JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, statePanel, fieldScroll);
        split.setBorder(null);
        split.setContinuousLayout(true);
        split.setOneTouchExpandable(true);
        split.setResizeWeight(0.66);

        // ---------- operation log ----------
        logArea = new JTextArea();
        logArea.setEditable(false);
        logArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
        JScrollPane logScroll = new JScrollPane(logArea);
        logScroll.setBorder(new TitledBorder("Operation log"));
        logScroll.setPreferredSize(new Dimension(0, 180));

        frame.add(control, BorderLayout.NORTH);
        frame.add(split, BorderLayout.CENTER);
        frame.add(logScroll, BorderLayout.SOUTH);

        // ---------- listeners ----------
        sizeCombo.addActionListener(e -> onPresetChanged());
        ruleCombo.addActionListener(e -> onPresetChanged());
        createButton.addActionListener(e -> doCreate());
        getButton.addActionListener(e -> doGet());
        indexOfButton.addActionListener(e -> doIndexOf());
        addButton.addActionListener(e -> doAdd());
        removeButton.addActionListener(e -> doRemove());
        clearButton.addActionListener(e -> doClear());
        resetButton.addActionListener(e -> resetDemo());

        reflectionBlind = !canReadInternals();
        onPresetChanged();
        setNoListState();
        printStartupLog();
        refreshView();
        if (reflectionBlind) {
            showAccessError();
        }

        frame.setSize(1320, 800);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
        // proportional divider position needs the final component size
        split.setDividerLocation(0.64);
    }

    // ---- operations ----

    private void doCreate() {
        clearError();
        Integer[] elements = currentElements();
        String call = callText(elements);
        List<Integer> created;
        try {
            created = List.of(elements);
        } catch (RuntimeException ex) {
            // Nulls are rejected before the list is built, so the previous
            // list (if any) stays on screen.
            String detail = exceptionText(ex);
            logLine(String.format("%-56s threw %s", call, detail));
            showError(call + " -> " + detail);
            refreshView();
            return;
        }
        list = created;
        lastTouchedSlot = -1;
        probePath = List.of();
        rebuildChoices(created);
        rebuildIndices(created.size());
        setListCreatedState();

        logLine(String.format("%-56s %s", call, created.size() <= 2
                ? created.getClass().getSimpleName() + ", no array"
                : "ListN, size " + created.size() + ", elements length " + created.size()));
        refreshView();
    }

    private void doGet() {
        clearError();
        IndexChoice index = selectedIndexChoice();
        String call = "get(" + index.value() + ")";
        lastTouchedSlot = -1;
        probePath = List.of();
        try {
            Integer value = list.get(index.value());
            lastTouchedSlot = index.value();
            logLine(String.format("%-56s %s", call, format(value)));
        } catch (RuntimeException ex) {
            // List12 checks the index itself; ListN lets the array do it.
            String detail = exceptionText(ex);
            logLine(String.format("%-56s threw %s", call, detail));
            showError(call + " -> " + detail);
        }
        refreshView();
    }

    private void doIndexOf() {
        clearError();
        ElementChoice c = selectedChoice();
        String call = "indexOf(" + format(c.value()) + ")";
        int size = list.size();
        List<Integer> visited = new ArrayList<>();
        int hit = -1;
        for (int i = 0; i < size; i++) {
            visited.add(i);
            if (list.get(i).equals(c.value())) {
                hit = i;
                break;
            }
        }
        probePath = visited;
        lastTouchedSlot = hit;
        String detail = (hit < 0 ? "-> -1 (not found)" : "-> " + hit)
                + "  (" + (visited.isEmpty() ? "empty list" : "compared "
                + visited.size() + " of " + size) + ")";
        logLine(String.format("%-56s %s", call, detail));
        refreshView();
    }

    private void doAdd() {
        clearError();
        ElementChoice c = selectedChoice();
        String call = "add(" + format(c.value()) + ")";
        lastTouchedSlot = -1;
        probePath = List.of();
        try {
            boolean changed = list.add(c.value());
            // Unreachable on JDK immutable lists; logged honestly if it ever is.
            logLine(String.format("%-56s returned %s (unexpected)", call, changed));
        } catch (UnsupportedOperationException ex) {
            String detail = exceptionText(ex);
            logLine(String.format("%-56s threw %s", call, detail));
            showError(call + " -> " + detail);
        }
        refreshView();
    }

    private void doRemove() {
        clearError();
        ElementChoice c = selectedChoice();
        String call = "remove(" + format(c.value()) + ")";
        lastTouchedSlot = -1;
        probePath = List.of();
        try {
            boolean changed = list.remove(c.value());
            logLine(String.format("%-56s returned %s (unexpected)", call, changed));
        } catch (UnsupportedOperationException ex) {
            String detail = exceptionText(ex);
            logLine(String.format("%-56s threw %s", call, detail));
            showError(call + " -> " + detail);
        }
        refreshView();
    }

    private void doClear() {
        clearError();
        lastTouchedSlot = -1;
        probePath = List.of();
        try {
            list.clear();
            logLine(String.format("%-56s returned void (unexpected)", "clear()"));
        } catch (UnsupportedOperationException ex) {
            String detail = exceptionText(ex);
            logLine(String.format("%-56s threw %s", "clear()", detail));
            showError("clear() -> " + detail);
        }
        refreshView();
    }

    private void resetDemo() {
        list = null;
        lastTouchedSlot = -1;
        probePath = List.of();
        slots = List.of();
        slotsFromList12 = false;
        emptySharedList = false;
        elementCombo.removeAllItems();
        indexCombo.removeAllItems();
        setNoListState();
        clearError();
        logArea.setText("");
        printStartupLog();
        refreshView();
    }

    private static String exceptionText(RuntimeException ex) {
        return ex.getClass().getName()
                + (ex.getMessage() == null ? " (no message)" : ": " + ex.getMessage());
    }

    /** Cheap startup preflight: no list instance is needed to test module access. */
    private static boolean canReadInternals() {
        try {
            Class.forName("java.util.ImmutableCollections$ListN")
                    .getDeclaredField("elements").setAccessible(true);
            return true;
        } catch (Throwable t) {
            return false;
        }
    }

    /** Every panel reads private fields, so block the UI instead of half-updating it. */
    private void showAccessError() {
        resetButton.setEnabled(false);
        setNoListState();
        errorBanner.setText("<html><b>Reflection access denied</b> --- this demo reads "
                + "ImmutableCollections$ListN.elements.<br>Restart with <code>java --add-opens="
                + "java.base/java.util=ALL-UNNAMED ListOfBehaviorDemo</code></html>");
        errorBanner.setVisible(true);
    }

    /** Banner plus a red border on the n / rule box; cleared by the next success. */
    private void showError(String message) {
        errorBanner.setText(message);
        errorBanner.setVisible(true);
        presetGroupBorder(true);
    }

    private void clearError() {
        if (reflectionBlind) {
            return; // keep the access banner up: no panel can work below it
        }
        errorBanner.setVisible(false);
        presetGroupBorder(false);
    }

    /** The n / rule pair is the only input that can throw, so it is the red one. */
    private void presetGroupBorder(boolean error) {
        presetGroupPanel.setBorder(error ? ERROR_BOX : PLAIN_BOX);
    }

    /** Re-derives the call text, gloss and preview whenever n or the rule changes. */
    private void onPresetChanged() {
        clearError();
        Rule rule = currentRule();
        // With zero arguments every rule produces the same empty list.
        ruleCombo.setEnabled(currentSize() > 0);
        Integer[] elements = currentElements();
        glossLabel.setText(currentSize() + " element(s)"
                + "  |  " + shapeOf(elements.length)
                + "  |  " + rule.note());
        String call = callText(elements);
        createButton.setText(call);
        createButton.setToolTipText("Runs: " + call);
        updateOpButtonText();
        refreshView();
    }

    private static String shapeOf(int n) {
        if (n == 0) {
            return "shared EMPTY_LIST (ListN, Object[0])";
        }
        if (n <= 2) {
            return "List12, two final fields (no array)";
        }
        return "ListN, elements length " + n + (n > 10 ? " (varargs overload)" : "");
    }

    /** Members in insertion order plus one absent probe; 0 is never a member. */
    private void rebuildChoices(List<Integer> created) {
        elementCombo.removeAllItems();
        for (Integer value : created) {
            elementCombo.addItem(new ElementChoice(format(value), value));
        }
        elementCombo.addItem(new ElementChoice(format(ABSENT_VALUE) + " (absent)", ABSENT_VALUE));
    }

    /** Valid indices plus one past the end, so get() can also show a failure. */
    private void rebuildIndices(int size) {
        indexCombo.removeAllItems();
        for (int i = 0; i < size; i++) {
            indexCombo.addItem(new IndexChoice(String.valueOf(i), i));
        }
        indexCombo.addItem(new IndexChoice(size + " (out of range)", size));
        updateOpButtonText();
    }

    private void updateOpButtonText() {
        IndexChoice index = selectedIndexChoice();
        getButton.setText(index == null ? "get(i)" : "get(" + index.value() + ")");
        ElementChoice c = selectedChoice();
        if (c == null) {
            return;
        }
        String v = format(c.value());
        indexOfButton.setText("indexOf(" + v + ")");
        addButton.setText("add(" + v + ")");
        removeButton.setText("remove(" + v + ")");
    }

    // ---- button state machine (same pattern as SetOfBehaviorDemo) ----

    private void setNoListState() {
        enableOps(false);
        enablePreset(!reflectionBlind);
        refreshButtonColors();
    }

    private void setListCreatedState() {
        enableOps(true);
        enablePreset(false);
        refreshButtonColors();
    }

    /** Demo setup and the factory button are one-shot: locked once a list exists. */
    private void enablePreset(boolean on) {
        sizeCombo.setEnabled(on);
        ruleCombo.setEnabled(on && currentSize() > 0);
        createButton.setEnabled(on);
    }

    /** One flag drives the whole ops rows; Reset is demo-only and stays available. */
    private void enableOps(boolean on) {
        indexCombo.setEnabled(on);
        elementCombo.setEnabled(on);
        for (JButton b : new JButton[]{getButton, indexOfButton, addButton, removeButton,
                clearButton}) {
            b.setEnabled(on);
        }
        refreshButtonColors();
    }

    /** Java-mapped buttons are blue, demo helpers dark gray, disabled turns light gray. */
    private void refreshButtonColors() {
        for (JButton b : new JButton[]{createButton, getButton, indexOfButton, addButton,
                removeButton, clearButton}) {
            b.setForeground(b.isEnabled() ? CODE_COLOR : DISABLED_COLOR);
        }
        resetButton.setForeground(resetButton.isEnabled() ? DEMO_COLOR : DISABLED_COLOR);
    }

    // ---- reflection access to java.util.ImmutableCollections ----

    /** Reads one private field of the created object; the only reflective call. */
    private static Object readField(Object target, String name) {
        try {
            Field f = target.getClass().getDeclaredField(name);
            f.setAccessible(true);
            return f.get(target);
        } catch (java.lang.reflect.InaccessibleObjectException e) {
            throw new IllegalStateException(
                    "cannot open java.base/java.util; run with "
                            + "--add-opens=java.base/java.util=ALL-UNNAMED", e);
        } catch (ReflectiveOperationException e) {
            throw new IllegalStateException("cannot read " + name + " on "
                    + target.getClass().getName(), e);
        }
    }

    private Integer[] readElements() {
        Object raw = readField(list, "elements");
        Object[] elements = (Object[]) raw;
        Integer[] values = new Integer[elements.length];
        for (int i = 0; i < elements.length; i++) {
            values[i] = (Integer) elements[i];
        }
        return values;
    }

    // ---- view refresh ----

    private void refreshView() {
        if (list == null) {
            runtimeClassLabel.setText(stateLine("runtime class", "--"));
            sizeLabel.setText(stateLine("size", "-- (call List.of(...) first)"));
            storageLabel.setText(stateLine("storage", "--"));
            accessLabel.setText(stateLine("get(i)", "--"));
            iterationLabel.setText(stateLine("iteration", "--"));
            toStringLabel.setText(stateLine("toString()", "--"));
            slots = List.of();
            slotGrid.updateForSlotCount(0);
            fieldShapeArea.setText(noListText());
            slotGrid.repaint();
            return;
        }

        boolean list12 = list.getClass().getSimpleName().endsWith("List12");
        slotsFromList12 = list12;
        if (list12) {
            slots = buildList12Slots();
            emptySharedList = false;
            runtimeClassLabel.setText(stateLine("runtime class", list.getClass().getName()));
            sizeLabel.setText(stateLine("size", String.valueOf(list.size())
                    + (list.size() == 1 ? "  (e1 == EMPTY, no count field)" : "")));
            storageLabel.setText(stateLine("storage", "final Object e0, e1 (no array)"));
            accessLabel.setText(stateLine("get(i)", "e0 / e1 branch, O(1)"));
        } else {
            Integer[] elements = readElements();
            Boolean allowNulls = (Boolean) readField(list, "allowNulls");
            emptySharedList = elements.length == 0;
            slots = buildListNSlots(elements);
            runtimeClassLabel.setText(stateLine("runtime class", list.getClass().getName()));
            sizeLabel.setText(stateLine("size", String.valueOf(list.size())));
            storageLabel.setText(stateLine("storage", "Object[] elements, length "
                    + elements.length + ", allowNulls = " + allowNulls));
            accessLabel.setText(stateLine("get(i)", "elements[i], O(1), no probing"));
        }
        iterationLabel.setText(stateLine("iteration",
                "insertion order, deterministic"
                        + (emptySharedList ? "  |  shared EMPTY_LIST" : "")));
        toStringLabel.setText(stateLine("toString()", list.toString()));

        fieldShapeArea.setText(buildFieldsText());
        fieldShapeArea.setCaretPosition(0);
        slotGrid.updateForSlotCount(slots.size());
        slotGrid.repaint();
    }

    /** List12 keeps two final fields; a one-element list stores the EMPTY sentinel in e1. */
    private List<Slot> buildList12Slots() {
        Integer e0 = (Integer) readField(list, "e0");
        Object rawE1 = readField(list, "e1");
        boolean sentinel = list.size() == 1;
        String e1Value = sentinel ? "<EMPTY>" : format((Integer) rawE1);
        return List.of(
                new Slot(0, "e0", format(e0), "index 0", false),
                new Slot(1, "e1", e1Value,
                        sentinel ? "shared sentinel\nsize() == 1" : "index 1",
                        sentinel));
    }

    private static List<Slot> buildListNSlots(Integer[] elements) {
        List<Slot> result = new ArrayList<>();
        for (int i = 0; i < elements.length; i++) {
            result.add(new Slot(i, "[" + i + "]", format(elements[i]),
                    "elements[" + i + "]", false));
        }
        return result;
    }

    private String buildFieldsText() {
        StringBuilder sb = new StringBuilder();
        sb.append(list.getClass().getName()).append('\n');
        if (slotsFromList12) {
            sb.append("  private final Object e0;    // ").append(slots.get(0).value()).append('\n');
            sb.append("  private final Object e1;    // ").append(slots.get(1).value()).append('\n');
            sb.append("  // no array: size() is e1 != EMPTY ? 2 : 1, so no count field\n")
              .append("  // get(0) reads e0, get(1) reads e1 and fails when e1 is EMPTY\n");
            return sb.toString();
        }

        int size = slots.size();
        sb.append("  private final Object[] elements;  // length ").append(size).append('\n');
        sb.append("  private final boolean allowNulls; // false: List.of rejects nulls\n");
        sb.append('\n');
        if (size == 0) {
            sb.append("  // List.of() returns the shared ImmutableCollections.EMPTY_LIST:\n")
              .append("  // List.of() == List.of() is ")
              .append(List.of() == List.of()).append('\n');
            return sb.toString();
        }
        sb.append("  // elements[i] is list.get(i): index order == insertion order\n");
        return sb.toString();
    }

    private static String noListText() {
        return "nothing created yet\n\nclick List.of(...) above: this panel then prints"
                + " the fields read from the actual object.\n";
    }

    private void printStartupLog() {
        logLine("what to observe:");
        logLine("  List.of()         -> shared EMPTY_LIST: a ListN with elements = Object[0]");
        logLine("  List.of(e1)       -> List12 with e1 = EMPTY, so size() needs no count field");
        logLine("  List.of(e1, e2)   -> List12 with two final fields, no array");
        logLine("  List.of(e1..eN)   -> ListN with elements = new Object[N] (exactly N, no padding)");
        logLine("  get(i)            -> elements[i]; List12 branches on i, no probing");
        logLine("  indexOf(v)        -> linear scan from 0 with equals; first match wins");
        logLine("  null elements     -> List.of rejects them with NullPointerException");
        logLine("  out of range      -> List12: IndexOutOfBoundsException, ListN: ArrayIndexOutOfBoundsException");
        logLine("  add/remove/clear  -> UnsupportedOperationException: the list is immutable");
        logLine("  iteration order   -> insertion order, identical on every run (no SALT)");
        logLine("  reflection        -> needs --add-opens=java.base/java.util=ALL-UNNAMED");
        logLine("");
    }

    /** Uniform "key = value" layout; keys padded so the '=' column aligns. */
    private static String stateLine(String key, String value) {
        return String.format("%-15s= %s", key, value);
    }

    private void logLine(String line) {
        logArea.append(line + System.lineSeparator());
        logArea.setCaretPosition(logArea.getDocument().getLength());
    }

    /** Left-hand caption of a button row; fixed width keeps rows aligned. */
    private static JLabel rowCaption(String text, Color color) {
        JLabel label = new JLabel(text);
        label.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 11));
        label.setForeground(color);
        label.setPreferredSize(new Dimension(96, 18));
        return label;
    }

    private static JPanel row(String caption, Color color, Component... items) {
        JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT));
        p.setAlignmentX(Component.LEFT_ALIGNMENT);
        p.add(rowCaption(caption, color));
        for (Component c : items) {
            p.add(c);
        }
        return p;
    }

    private Rule currentRule() {
        return (Rule) ruleCombo.getSelectedItem();
    }

    private int currentSize() {
        Integer n = (Integer) sizeCombo.getSelectedItem();
        return n == null ? 0 : n;
    }

    private Integer[] currentElements() {
        return currentRule().gen().apply(currentSize());
    }

    private IndexChoice selectedIndexChoice() {
        return (IndexChoice) indexCombo.getSelectedItem();
    }

    private ElementChoice selectedChoice() {
        return (ElementChoice) elementCombo.getSelectedItem();
    }

    private static String callText(Integer[] elements) {
        if (elements.length == 0) {
            return "List.of()";
        }
        StringBuilder sb = new StringBuilder("List.of(");
        for (int i = 0; i < elements.length; i++) {
            if (i > 0) {
                sb.append(", ");
            }
            sb.append(format(elements[i]));
        }
        return sb.append(')').toString();
    }

    /** Integers print as plain digits; null stays null, which makes List.of throw. */
    private static String format(Integer value) {
        return value == null ? "null" : value.toString();
    }

    // ---- slot grid: one cell per ListN array slot, or per List12 field ----

    /**
     * Index order (ListN array) or field order (List12 e0/e1). All cells are
     * filled, the last hit index is orange, and indexOf-compared cells get a
     * dashed amber border.
     */
    private final class SlotGrid extends JPanel {
        private static final int CELL_W = 96;
        private static final int CELL_H = 78;
        private static final int GAP = 8;
        private static final int PER_ROW = 8;
        /** Fixed panel width keeps the state panel stable across presets. */
        private static final int GRID_W = 830;

        SlotGrid() {
            updateForSlotCount(0);
            setBackground(Color.WHITE);
        }

        /** Grows the panel to as many rows as the current storage needs. */
        void updateForSlotCount(int count) {
            int rows = Math.max(1, (count + PER_ROW - 1) / PER_ROW);
            setPreferredSize(new Dimension(GRID_W, rows * (CELL_H + GAP) + GAP));
            revalidate();
        }

        @Override
        protected void paintComponent(Graphics g0) {
            super.paintComponent(g0);
            Graphics2D g = (Graphics2D) g0;
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

            if (slots.isEmpty()) {
                g.setColor(Color.GRAY);
                g.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
                g.drawString(list == null
                        ? "no list yet - click List.of(...) above"
                        : "elements = Object[0]: the shared EMPTY_LIST has no slots", 16, 28);
                return;
            }
            int cols = Math.min(slots.size(), PER_ROW);
            int rows = (slots.size() + PER_ROW - 1) / PER_ROW;
            int totalW = cols * CELL_W + (cols - 1) * GAP;
            int startX = Math.max(GAP, (getWidth() - totalW) / 2);

            for (int i = 0; i < slots.size(); i++) {
                int row = i / PER_ROW;
                int col = i % PER_ROW;
                int x = startX + col * (CELL_W + GAP);
                int y = GAP + row * (CELL_H + GAP);
                paintSlot(g, slots.get(i), x, y);
            }
        }

        private void paintSlot(Graphics2D g, Slot slot, int x, int y) {
            boolean onPath = probePath.contains(slot.index());
            boolean touched = slot.index() == lastTouchedSlot;

            g.setColor(slot.sentinel() ? SENTINEL_BG : SLOT_FILLED_COLOR);
            g.fillRect(x, y, CELL_W, CELL_H);

            if (touched) {
                g.setColor(LAST_TOUCHED_COLOR);
                g.setStroke(new BasicStroke(3));
            } else if (onPath) {
                g.setColor(LAST_TOUCHED_COLOR);
                g.setStroke(new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
                        10f, new float[]{6f, 4f}, 0f));
            } else {
                g.setColor(new Color(180, 180, 180));
                g.setStroke(new BasicStroke(1));
            }
            g.drawRect(x, y, CELL_W, CELL_H);

            // slot label: [i] for array slots, e0 / e1 for List12 fields
            g.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 10));
            g.setColor(slot.sentinel() ? SENTINEL_FG : Color.WHITE);
            g.drawString(slot.title(), x + 6, y + 14);

            // element
            g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
            FontMetrics fm = g.getFontMetrics();
            String text = ellipsize(slot.value(), fm, CELL_W - 12);
            g.setColor(slot.sentinel() ? SENTINEL_FG : Color.WHITE);
            g.drawString(text, x + (CELL_W - fm.stringWidth(text)) / 2, y + 44);

            // note: the field expression / logical index, one line per fact
            if (slot.note() != null && !slot.note().isEmpty()) {
                g.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 9));
                g.setColor(slot.sentinel() ? SENTINEL_FG : Color.WHITE);
                FontMetrics nfm = g.getFontMetrics();
                String[] lines = slot.note().split("\n");
                int baseline = y + (lines.length > 1 ? 56 : 66);
                for (int i = 0; i < lines.length; i++) {
                    String line = ellipsize(lines[i], nfm, CELL_W - 10);
                    g.drawString(line, x + (CELL_W - nfm.stringWidth(line)) / 2, baseline + i * 12);
                }
            }
        }

        private static String ellipsize(String text, FontMetrics fm, int maxWidth) {
            if (fm.stringWidth(text) <= maxWidth) {
                return text;
            }
            String cut = text;
            while (cut.length() > 1 && fm.stringWidth(cut + "..") > maxWidth) {
                cut = cut.substring(0, cut.length() - 1);
            }
            return cut + "..";
        }
    }
}
相关推荐
苏三说技术26 分钟前
Codex中必装的11个Skills
后端
狼爷1 小时前
别再只刷MMLU了:真正的AI能力评测,早已从“做题”走向“干活” —— 深度解读 Artificial Analysis 智能指数 v5.0 方法论
ai编程
付威20231 小时前
target 目录越来越大,Rust 项目该怎么清理
后端
技录局2 小时前
Kafka 深度原理分析:从消息流到分布式日志
后端
大数据技术派2 小时前
我用AI写代码,做了3个小玩意儿
后端·微信小程序
yeccheng2 小时前
MySQL 索引超详解:从底层 B+ 树到面试高频考点
java·开发语言·jvm·mysql·面试
这个昵称不容易2 小时前
SpringBoot 读书笔记共享平台部署实战:97 个 Java 文件 / 113 个接口 / 10 张表
java·springboot·毕设
明月_清风3 小时前
Meta 开源了一个 AI Native 组件库:Astryx,150+ React 组件,专为 AI Agent 而设计
前端·人工智能·ai编程
爱上咖啡香3 小时前
我给 Laravel AI 写了个千问驱动,文本、语音、图像、视频全打通
后端