[Java] 用 GUI 展示 ArrayList 扩容的过程

0 阅读5分钟

背景

我之前写过 [Java] 验证 ArrayList 的扩容机制 一文,那篇文章通过命令行来展示 ArrayList 的扩容过程。考虑到现在 AI 编程非常方便,我可以借助 trae 把原有的代码改造成 GUI 版本。

正文

完整的代码

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.lang.reflect.Field;
import java.lang.reflect.InaccessibleObjectException;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.border.TitledBorder;

/**
 * ArrayList 扩容过程可视化演示。
 * <p>
 * 原理与掘金文章《验证 ArrayList 的扩容机制》相同: 通过反射读取
 * ArrayList 的 elementData(实际 Object[] 数组)与 size 字段,
 * 观察每次 add 之后容量(capacity = elementData.length)的变化:
 * <p>
 * 1) 无参构造时 elementData 是长度为 0 的空数组(懒加载);
 * 2) 第一次 add 时才分配数组, 容量直接为 DEFAULT_CAPACITY(10);
 * 3) 之后容量不足时按 1.5 倍扩容:
 * newCapacity = oldCapacity + (oldCapacity >> 1)
 * 即 10 -> 15 -> 22 -> 33 -> 49 -> 73 -> ...
 * <p>
 * JDK 9 及以上版本运行需要开放 java.util 包的反射权限:
 * javac ArrayListGrowthDemo.java
 * java --add-opens=java.base/java.util=ALL-UNNAMED ArrayListGrowthDemo
 */
public class ArrayListGrowthDemo {

    /**
     * ArrayList.DEFAULT_CAPACITY, 首次 add 时的默认容量。
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 自动播放时最多添加的元素个数(足够展示多次扩容)。
     */
    private static final int AUTO_STOP = 55;

    private static final int COLS = 10;
    private static final int CELL_W = 64;
    private static final int CELL_H = 48;
    private static final int GAP = 6;

    private static final Color FILLED_COLOR = new Color(66, 133, 244);
    private static final Color NEW_SLOT_COLOR = new Color(209, 240, 214);
    private static final Color EMPTY_COLOR = new Color(240, 240, 240);
    private static final Color LAST_ADDED_COLOR = new Color(244, 180, 0);

    private final JFrame frame = new JFrame("ArrayList 扩容过程演示");

    private List<Object> list;
    private int addedCount;

    // 最近一次扩容中新开辟的槽位区间 [newSlotStart, newSlotEnd]
    private int newSlotStart = -1;
    private int newSlotEnd = -1;
    // 最近一次 add 放入元素的槽位
    private int lastAddedIndex = -1;
    private boolean grewLastTick = false;

    private final Field elementDataField;
    private final Field sizeField;

    private JButton addButton;
    private JButton autoButton;
    private JLabel sizeLabel;
    private JLabel capacityLabel;
    private JLabel remainLabel;
    private JLabel messageLabel;
    private JTextArea logArea;
    private ArrayGrid grid;
    private Timer timer;

    public ArrayListGrowthDemo() throws ReflectiveOperationException {
        // 与文章一致: 通过反射获取 ArrayList 内部数组和 size 字段
        elementDataField = ArrayList.class.getDeclaredField("elementData");
        sizeField = ArrayList.class.getDeclaredField("size");
        elementDataField.setAccessible(true);
        sizeField.setAccessible(true);
        buildUi();
        resetList();
    }

    private void buildUi() {
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(900, 680);
        frame.setLocationRelativeTo(null);

        // ---------- 顶部控制区 ----------
        JPanel control = new JPanel();
        control.setLayout(new BoxLayout(control, BoxLayout.Y_AXIS));

        JPanel row1 = new JPanel();
        addButton = new JButton("添加 1 个元素");
        autoButton = new JButton("自动播放");
        JButton resetButton = new JButton("重置");
        row1.add(addButton);
        row1.add(autoButton);
        row1.add(resetButton);
        control.add(row1);

        JPanel row2 = new JPanel();
        sizeLabel = new JLabel();
        capacityLabel = new JLabel();
        remainLabel = new JLabel();
        Font bold = new Font(Font.SANS_SERIF, Font.BOLD, 13);
        sizeLabel.setFont(bold);
        capacityLabel.setFont(bold);
        remainLabel.setFont(bold);
        row2.add(sizeLabel);
        row2.add(Box.createHorizontalStrut(30));
        row2.add(capacityLabel);
        row2.add(Box.createHorizontalStrut(30));
        row2.add(remainLabel);
        control.add(row2);

        messageLabel = new JLabel(" ", JLabel.CENTER);
        messageLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 14));
        messageLabel.setForeground(new Color(216, 67, 21));
        control.add(messageLabel);

        frame.add(control, BorderLayout.NORTH);

        // ---------- 中部: 数组槽位可视化 ----------
        grid = new ArrayGrid();
        JScrollPane scrollPane = new JScrollPane(grid);
        scrollPane.setBorder(new TitledBorder("elementData 数组 (capacity = 数组长度, size = 已用槽位)"));
        scrollPane.getVerticalScrollBar().setUnitIncrement(16);
        frame.add(scrollPane, BorderLayout.CENTER);

        // ---------- 底部: 扩容日志 ----------
        logArea = new JTextArea(8, 20);
        logArea.setEditable(false);
        logArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
        JScrollPane logScroll = new JScrollPane(logArea);
        logScroll.setBorder(new TitledBorder("扩容事件日志 (与命令行分析器输出一致)"));
        frame.add(logScroll, BorderLayout.SOUTH);

        addButton.addActionListener(e -> doAdd());
        resetButton.addActionListener(e -> resetList());
        autoButton.addActionListener(e -> toggleAuto());

        timer = new Timer(220, e -> {
            doAdd();
            if (addedCount >= AUTO_STOP) {
                timer.stop();
                autoButton.setText("自动播放");
                addButton.setEnabled(true);
                logLine("--- 自动播放结束, 可继续点击 "添加 1 个元素" ---");
            }
            // 扩容发生时放慢一拍, 便于观察绿色新槽位
            timer.setDelay(grewLastTick ? 900 : 220);
        });
    }

    /**
     * 执行一次 add, 并对比 add 前后的容量。
     */
    private void doAdd() {
        int oldCapacity = getCapacity();
        int oldSize = getSize();

        list.add(addedCount);
        addedCount++;

        int newCapacity = getCapacity();
        int newSize = getSize();
        grewLastTick = newCapacity > oldCapacity;
        lastAddedIndex = oldSize;

        if (grewLastTick) {
            newSlotStart = oldCapacity;
            newSlotEnd = newCapacity - 1;
            if (oldCapacity == 0) {
                messageLabel.setText(String.format(
                        "扩容! 首次 add 触发懒加载, capacity %d -> %d (DEFAULT_CAPACITY)",
                        oldCapacity, newCapacity));
                logLine(String.format(
                        "size %2d -> %-2d  capacity %2d -> %-3d  (懒加载, 直接分配默认容量 %d)",
                        oldSize, newSize, oldCapacity, newCapacity, DEFAULT_CAPACITY));
            } else {
                messageLabel.setText(String.format(
                        "扩容! size %d -> %d 时数组已满, capacity %d -> %d (新容量 = 旧容量 + 旧容量>>1)",
                        oldSize, newSize, oldCapacity, newCapacity));
                logLine(String.format(
                        "size %2d -> %-2d  capacity %2d -> %-3d  (新容量 = %d + %d = %d)",
                        oldSize, newSize, oldCapacity, newCapacity,
                        oldCapacity, oldCapacity >> 1, newCapacity));
            }
        } else {
            messageLabel.setText(" ");
        }

        refreshView();
    }

    private void toggleAuto() {
        if (timer.isRunning()) {
            timer.stop();
            autoButton.setText("自动播放");
            addButton.setEnabled(true);
        } else {
            if (addedCount >= AUTO_STOP) {
                resetList();
            }
            timer.start();
            autoButton.setText("暂停");
            addButton.setEnabled(false);
        }
    }

    private void resetList() {
        if (timer != null) {
            timer.stop();
        }
        autoButton.setText("自动播放");
        addButton.setEnabled(true);

        list = new ArrayList<>();
        addedCount = 0;
        newSlotStart = newSlotEnd = -1;
        lastAddedIndex = -1;
        grewLastTick = false;
        messageLabel.setText(" ");
        logArea.setText("");
        logLine("已创建: new ArrayList<>()");
        logLine("观察点: 无参构造时 capacity 初始为 0, 第 1 次 add 才变为 10;");
        logLine("        此后每当 size == capacity, 按 1.5 倍扩容 (Arrays.copyOf 复制旧数组)。");
        logLine("");
        refreshView();
    }

    private void refreshView() {
        int size = getSize();
        int capacity = getCapacity();
        sizeLabel.setText("size (元素个数): " + size);
        capacityLabel.setText("capacity (数组长度): " + capacity);
        remainLabel.setText("剩余空位: " + (capacity - size));
        grid.setData(list, capacity, size);
    }

    private int getCapacity() {
        try {
            Object[] elementData = (Object[]) elementDataField.get(list);
            return elementData.length;
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    private int getSize() {
        try {
            return (int) sizeField.get(list);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

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

    /**
     * 用方块绘制 elementData 数组的每个槽位。
     */
    private class ArrayGrid extends JPanel {

        private List<Object> data = new ArrayList<>();
        private int capacity;
        private int size;

        ArrayGrid() {
            setBackground(Color.WHITE);
        }

        void setData(List<Object> data, int capacity, int size) {
            this.data = data;
            this.capacity = capacity;
            this.size = size;
            int rows = Math.max(1, (capacity + COLS - 1) / COLS);
            setPreferredSize(new Dimension(
                    COLS * CELL_W + (COLS + 1) * GAP,
                    rows * CELL_H + (rows + 1) * GAP));
            revalidate();
            repaint();
        }

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

            if (capacity == 0) {
                g2.setColor(Color.GRAY);
                g2.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 14));
                g2.drawString("elementData 当前是空数组 (length = 0), 点击 "添加 1 个元素" 观察首次分配",
                        GAP, 30);
                return;
            }

            Font valueFont = new Font(Font.SANS_SERIF, Font.BOLD, 15);
            Font indexFont = new Font(Font.MONOSPACED, Font.PLAIN, 10);

            for (int i = 0; i < capacity; i++) {
                int col = i % COLS;
                int row = i / COLS;
                int x = GAP + col * (CELL_W + GAP);
                int y = GAP + row * (CELL_H + GAP);

                boolean filled = i < size;
                boolean isNewSlot = i >= newSlotStart && i <= newSlotEnd;

                if (filled) {
                    g2.setColor(FILLED_COLOR);
                } else if (isNewSlot) {
                    g2.setColor(NEW_SLOT_COLOR);
                } else {
                    g2.setColor(EMPTY_COLOR);
                }
                g2.fillRoundRect(x, y, CELL_W, CELL_H, 8, 8);

                // 槽位下标
                g2.setColor(filled ? Color.WHITE : Color.GRAY);
                g2.setFont(indexFont);
                g2.drawString("[" + i + "]", x + 4, y + 12);

                // 元素值或 null
                g2.setFont(valueFont);
                if (filled) {
                    g2.setColor(Color.WHITE);
                    g2.drawString(String.valueOf(data.get(i)), x + 22, y + 32);
                } else {
                    g2.setColor(new Color(150, 150, 150));
                    g2.drawString("null", x + 18, y + 32);
                }

                // 最近一次放入元素的槽位用橙色边框标出
                if (i == lastAddedIndex) {
                    g2.setColor(LAST_ADDED_COLOR);
                    g2.drawRoundRect(x - 2, y - 2, CELL_W + 4, CELL_H + 4, 10, 10);
                    g2.drawRoundRect(x - 3, y - 3, CELL_W + 6, CELL_H + 6, 12, 12);
                }
            }
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            ArrayListGrowthDemo demo;
            try {
                demo = new ArrayListGrowthDemo();
            } catch (InaccessibleObjectException e) {
                JOptionPane.showMessageDialog(null,
                        """
                                反射访问 ArrayList 内部字段被拒绝。
                                
                                JDK 9 及以上请使用以下命令启动:
                                java --add-opens=java.base/java.util=ALL-UNNAMED \
                                ArrayListGrowthDemo""",
                        "需要 --add-opens 参数", JOptionPane.ERROR_MESSAGE);
                return;
            } catch (ReflectiveOperationException e) {
                JOptionPane.showMessageDialog(null,
                        "初始化失败: " + e.getMessage(),
                        "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            demo.frame.setVisible(true);
        });
    }
}

如何运行

请将上述 java 代码保存为 ArrayListGrowthDemo.java。使用以下命令可以编译 ArrayListGrowthDemo.java

# 说明: 我本地使用的 javac 版本是 21
javac ArrayListGrowthDemo.java

编译成功后,会看到以下两个 class 文件

  • ArrayListGrowthDemo.class
  • ArrayListGrowthDemo$ArrayGrid.class

使用如下命令可以运行 ArrayListGrowthDemo 中的 main 方法

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

运行效果

初始的运行效果如下 ⬇️

image.png

我们可以通过点击“添加 1 个元素”按钮来逐个添加元素,示例效果如下 ⬇️

image.png

也可以点击“自动播放”按钮以避免枯燥的重复操作 ⬇️

image.png

参考资料