背景
我之前写过 [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.classArrayListGrowthDemo$ArrayGrid.class
使用如下命令可以运行 ArrayListGrowthDemo 中的 main 方法
java --add-opens=java.base/java.util=ALL-UNNAMED ArrayListGrowthDemo
运行效果
初始的运行效果如下 ⬇️
我们可以通过点击“添加 1 个元素”按钮来逐个添加元素,示例效果如下 ⬇️
也可以点击“自动播放”按钮以避免枯燥的重复操作 ⬇️