[Java] 展示增删元素时 LinkedList 的内部变化

背景

我之前写过 Java 展示添加元素时 ArrayList 的内部变化,用来展示添加元素时 ArrayList 的内部变化。后来我想到 LinkedList 在增删元素时,我们也可以用 GUI 的方式来呈现其内部的变化。于是我借助 trae 完成了这样的 java 代码。

注意事项

完整代码 里用到了 java 21 新增的 API,在您编译和运行本文提供的 java 代码时,请确保您所使用的 javac/java 版本不低于 21。

正文

如何运行

为了行文连贯,我把完整的代码放到 这一小节 里了,请将完整的代码保存为 LinkedListBehaviorDemo.java。使用如下命令可以编译 LinkedListBehaviorDemo.java。

bash 复制代码
javac LinkedListBehaviorDemo.java

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

  • LinkedListBehaviorDemo.class
  • LinkedListBehaviorDemo$ChainView.class
  • LinkedListBehaviorDemo$NodeView.class

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

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

运行效果示例

初始效果

构造函数

可以使用默认构造函数 ⬇️

添加元素

通过点击以下按钮,我们可以向 LinkedList 中添加一个元素

  • add("A") 按钮(按钮中的字符串会不断变化,最初是 "A")
  • addFirst("A") 按钮(按钮中的字符串会不断变化,最初是 "A")
  • add(i, "A") 按钮(按钮中的字符串会不断变化,最初是 "A")

点击这些按钮后,LinkedList 里的对应方法会被调用 ⬇️

添加元素后的示例效果如下 ⬇️

删除元素

通过点击以下按钮,我们可以从 LinkedList 中删除一个元素

  • removeFirst() 按钮
  • removeLast() 按钮
  • remove(i) 按钮

点击这些按钮后,LinkedList 里的对应方法会被调用 ⬇️

Show logical chain 按钮

这个按钮可以展示各个节点逻辑上的前后顺序(会将 next 引用以及 prev 引用都展示出来)

Show heap scatter 按钮

这个按钮用于展示各个节点的分布(所展示的并非各个节点真实的地址)。我们可以指定展示各个节点的 next 引用

也可以指定展示各个节点的 prev 引用

Reset 按钮

这个按钮用于重新开始

Current state

这个控件里会展示 LinkedList 的 size/first/last 三个字段

size/first/last 来自 LinkedList ⬇️

完整的代码

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.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.QuadCurve2D;
import java.lang.reflect.Field;
import java.lang.reflect.InaccessibleObjectException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTextArea;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;
import javax.swing.border.TitledBorder;

/**
 * LinkedList behavior demo: visualizes the private internals of
 * java.util.LinkedList while add / remove are called.
 * <p>
 * Same principle as ArrayListBehaviorDemo, but the interesting fields are
 * different. LinkedList keeps:
 * <pre>
 * transient Node&lt;E&gt; first;
 * transient Node&lt;E&gt; last;
 * transient int     size;
 * </pre>
 * and every node is a separate object:
 * <pre>
 * private static class Node&lt;E&gt; { E item; Node&lt;E&gt; next; Node&lt;E&gt; prev; }
 * </pre>
 * The nodes are NOT stored in one contiguous array: each one is an
 * independent heap object, and the list order exists only in the
 * next/prev references. The GUI therefore draws two things at once:
 * <ul>
 *   <li>the logical chain (box per node, forward next arrow, backward
 *       prev arrow, null terminated at both ends);</li>
 *   <li>an optional scattered layout, where each node is placed at a
 *       pseudo-position derived from its identity hash, so it becomes
 *       visible that neighbors in the list are not neighbors in memory.</li>
 * </ul>
 * Observable facts shown by the demo:
 * <ol>
 *   <li>add(E) / addLast(E) links a new node after "last";</li>
 *   <li>addFirst(E) links a new node before "first";</li>
 *   <li>add(i, E) first calls node(i) and walks the links, then re-links
 *       two neighbors (index == size skips the walk and links at the tail);</li>
 *   <li>removeFirst() / removeLast() unlink one end;</li>
 *   <li>remove(i) also walks with node(i), then unlinks;</li>
 *   <li>both walks go forward from first via next when i &lt; size/2, otherwise
 *       backward from last via prev, so reaching an index costs O(n) - the
 *       reason ArrayList wins at random access.</li>
 * </ol>
 * On JDK 9+ run with:
 * javac LinkedListBehaviorDemo.java
 * java --add-opens=java.base/java.util=ALL-UNNAMED LinkedListBehaviorDemo
 */
public class LinkedListBehaviorDemo {

    /**
     * Value used by the next insert. Distinct values make the insertion
     * position readable: after inserting "C" you see A, B, C, D.
     */
    private int nextLabel;

    // ---- chain layout (one box per node, laid out in list order) ----
    private static final int NODE_W = 96;
    private static final int NODE_H = 40;
    private static final int CHAIN_GAP = 58;
    private static final int CHAIN_PITCH = NODE_W + CHAIN_GAP;
    private static final int CHAIN_LEFT = 84;
    private static final int CHAIN_TOP = 78;

    // ---- scattered layout (nodes at pseudo heap positions) ----
    private static final int SCATTER_COLS = 5;
    private static final int SLOT_W = 148;
    private static final int SLOT_H = 98;
    private static final int MIN_SCATTER_ROWS = 2;

    private static final Color NODE_COLOR = new Color(66, 133, 244);
    private static final Color OUTLINE_COLOR = new Color(170, 170, 170);
    private static final Color TOUCHED_COLOR = new Color(244, 180, 0);
    /** next (forward) links: navy, so the two directions stay distinguishable. */
    private static final Color NEXT_COLOR = new Color(26, 60, 130);
    /** prev (backward) links: magenta. */
    private static final Color PREV_COLOR = new Color(173, 20, 87);
    private static final Color FAINT_COLOR = new Color(210, 210, 210);

    /** 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. */
    private static final Color DEMO_COLOR = new Color(93, 93, 93);
    /** Rejected operations: the same red is used for the banner and the offending control. */
    private static final Color REJECT_COLOR = new Color(198, 40, 40);

    private static final Border GROUP_BORDER = BorderFactory.createEtchedBorder();
    private static final Border GROUP_REJECT_BORDER = BorderFactory.createLineBorder(REJECT_COLOR, 2);

    /** Dashed shaft for null terminators: a dashed line "leads nowhere", unlike a solid reference. */
    private static final Stroke NULL_STROKE = new BasicStroke(1.5f, BasicStroke.CAP_BUTT,
            BasicStroke.JOIN_MITER, 10f, new float[]{4f, 3f}, 0f);
    /** Solid shaft for links between two real nodes. */
    private static final Stroke LINK_STROKE = new BasicStroke(1.7f);

    private final JFrame frame = new JFrame("LinkedList Behavior Demo");

    /**
     * The list under observation. Deliberately null until the constructor
     * button is pressed, so "no instance yet" is a visible state too.
     */
    private LinkedList<String> list;

    /** Highlight: index of the node created by the last mutating call. */
    private int lastTouchedIndex = -1;

    private boolean scatterLayout;
    /** Which link direction the scatter view draws; meaningless in the chain view. */
    private boolean scatterShowPrev;

    private final Field firstField;
    private final Field sizeField;
    private final Field itemField;
    private final Field nextField;

    private JButton addButton;
    private JButton constructorButton;
    private JButton addFirstButton;
    private JButton addAtButton;
    private JButton removeFirstButton;
    private JButton removeLastButton;
    private JButton removeAtButton;
    private JButton layoutButton;
    private JRadioButton nextLinkRadio;
    private JRadioButton prevLinkRadio;
    private JButton resetButton;
    /** One index control per row, so a button only ever reads the "i" shown next to it. */
    private JSpinner addIndexSpinner;
    private JSpinner removeIndexSpinner;
    /** The bordered containers above, needed to mark the offending index control red. */
    private JPanel addIndexGroup;
    private JPanel removeIndexGroup;
    /** Banner that repeats a rejected operation until the next successful one. */
    private JLabel rejectLabel;
    private JLabel sizeLabel;
    private JLabel firstLabel;
    private JLabel lastLabel;
    private JTextArea logArea;
    private ChainView canvas;

    public LinkedListBehaviorDemo() throws ReflectiveOperationException {
        firstField = LinkedList.class.getDeclaredField("first");
        sizeField = LinkedList.class.getDeclaredField("size");
        // Node is a private static nested class, so its fields are read
        // through the class of the "first" field.
        Class<?> nodeClass = firstField.getType();
        itemField = nodeClass.getDeclaredField("item");
        nextField = nodeClass.getDeclaredField("next");
        for (Field f : new Field[]{firstField, sizeField, itemField, nextField}) {
            f.setAccessible(true);
        }
        buildUi();
        refreshView();
    }

    // ======================================================================
    // UI
    // ======================================================================

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

        JPanel control = new JPanel();
        control.setLayout(new BoxLayout(control, BoxLayout.Y_AXIS));

        Font codeFont = new Font(Font.MONOSPACED, Font.BOLD, 13);

        // ---------- row 1: constructor ----------
        JPanel constructorRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
        constructorRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        constructorButton = new JButton("new LinkedList<String>()");
        constructorButton.setFont(codeFont);
        constructorButton.setToolTipText("Runs: new LinkedList<String>() - disabled while an "
                + "instance exists; use Reset to start over");
        constructorRow.add(rowCaption("constructor", CODE_COLOR));
        constructorRow.add(constructorButton);
        control.add(constructorRow);

        // ---------- row 2: add ----------
        JPanel addRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
        addRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        addButton = new JButton();
        addFirstButton = new JButton();
        addAtButton = new JButton();
        for (JButton b : new JButton[]{addButton, addFirstButton, addAtButton}) {
            b.setFont(codeFont);
        }
        addButton.setToolTipText("Runs: list.add(value) - same as addLast: links a new node after the tail. "
                + "The value on the button is generated: A, B, C, ...");
        addFirstButton.setToolTipText("Runs: list.addFirst(value): links a new node before the head");
        addAtButton.setToolTipText("Runs: list.add(i, value) with i from the spinner on this row");
        updateAddButtonTexts();
        addRow.add(rowCaption("add", CODE_COLOR));
        addRow.add(addButton);
        addRow.add(addFirstButton);
        addIndexSpinner = createIndexSpinner();
        addIndexGroup = indexGroup(addAtButton, addIndexSpinner);
        addRow.add(addIndexGroup);
        control.add(addRow);

        // ---------- row 3: remove ----------
        JPanel removeRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
        removeRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        removeFirstButton = new JButton("removeFirst()");
        removeLastButton = new JButton("removeLast()");
        removeAtButton = new JButton("remove(i)");
        for (JButton b : new JButton[]{removeFirstButton, removeLastButton, removeAtButton}) {
            b.setFont(codeFont);
        }
        removeFirstButton.setToolTipText("Runs: list.removeFirst()");
        removeLastButton.setToolTipText("Runs: list.removeLast()");
        removeAtButton.setToolTipText("Runs: list.remove(i) with i from the spinner on this row");
        removeRow.add(rowCaption("remove", CODE_COLOR));
        removeRow.add(removeFirstButton);
        removeRow.add(removeLastButton);
        removeIndexSpinner = createIndexSpinner();
        removeIndexGroup = indexGroup(removeAtButton, removeIndexSpinner);
        removeRow.add(removeIndexGroup);
        control.add(removeRow);

        // ---------- row 4: demo only ----------
        JPanel demoRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
        demoRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        layoutButton = new JButton("Show heap scatter");
        layoutButton.setToolTipText("Demo helper, not a Java API call: draws the same list at "
                + "pseudo-positions derived from each node's identity hash, to show that "
                + "linked neighbours are not adjacent objects");
        layoutButton.setForeground(DEMO_COLOR);
        demoRow.add(rowCaption("demo only", DEMO_COLOR));
        demoRow.add(layoutButton);

        // link-direction choice, only meaningful while the scatter is shown
        JLabel linksLabel = new JLabel("show links:");
        nextLinkRadio = new JRadioButton("next", true);
        prevLinkRadio = new JRadioButton("prev");
        nextLinkRadio.setForeground(NEXT_COLOR);
        prevLinkRadio.setForeground(PREV_COLOR);
        ButtonGroup linkGroup = new ButtonGroup();
        linkGroup.add(nextLinkRadio);
        linkGroup.add(prevLinkRadio);
        nextLinkRadio.addActionListener(e -> {
            scatterShowPrev = false;
            canvas.repaint();
        });
        prevLinkRadio.addActionListener(e -> {
            scatterShowPrev = true;
            canvas.repaint();
        });
        setLinkRadiosEnabled(false);
        demoRow.add(Box.createHorizontalStrut(16));
        demoRow.add(linksLabel);
        demoRow.add(nextLinkRadio);
        demoRow.add(prevLinkRadio);

        resetButton = new JButton("Reset");
        resetButton.setToolTipText("Demo helper, not a Java API call: discards the list, clears "
                + "the log and returns every control to the initial state "
                + "(only the constructor stays clickable)");
        resetButton.setForeground(DEMO_COLOR);
        demoRow.add(Box.createHorizontalStrut(16));
        demoRow.add(resetButton);
        control.add(demoRow);

        // ---------- status ----------
        JPanel statusRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
        statusRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        statusRow.setBorder(new TitledBorder("Current state"));
        sizeLabel = new JLabel();
        firstLabel = new JLabel();
        lastLabel = new JLabel();
        Font bold = new Font(Font.SANS_SERIF, Font.BOLD, 13);
        for (JLabel l : new JLabel[]{sizeLabel, firstLabel, lastLabel}) {
            l.setFont(bold);
        }
        statusRow.add(sizeLabel);
        statusRow.add(Box.createHorizontalStrut(24));
        statusRow.add(firstLabel);
        statusRow.add(Box.createHorizontalStrut(24));
        statusRow.add(lastLabel);
        control.add(statusRow);

        // ---------- rejected operation banner ----------
        // Always allocated so the rows above do not jump when it appears;
        // neutral and transparent until something is actually rejected.
        JPanel rejectRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
        rejectRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        rejectLabel = new JLabel(" ");
        rejectLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
        rejectLabel.setBorder(BorderFactory.createEmptyBorder(3, 8, 3, 8));
        rejectLabel.setOpaque(false);
        rejectRow.add(rejectLabel);
        control.add(rejectRow);

        frame.add(control, BorderLayout.NORTH);

        // ---------- center: the linked structure ----------
        canvas = new ChainView();
        JScrollPane canvasScroll = new JScrollPane(canvas);
        canvasScroll.setBorder(new TitledBorder("LinkedList internals: first / last / next / prev"));
        canvasScroll.getVerticalScrollBar().setUnitIncrement(16);
        canvasScroll.getHorizontalScrollBar().setUnitIncrement(16);
        frame.add(canvasScroll, BorderLayout.CENTER);

        // ---------- bottom: operation log ----------
        logArea = new JTextArea(9, 20);
        logArea.setEditable(false);
        logArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
        JScrollPane logScroll = new JScrollPane(logArea);
        logScroll.setBorder(new TitledBorder("Operation log"));
        frame.add(logScroll, BorderLayout.SOUTH);

        constructorButton.addActionListener(e -> doConstructor());
        addButton.addActionListener(e -> doAdd());
        addFirstButton.addActionListener(e -> doAddFirst());
        addAtButton.addActionListener(e -> doAddAt());
        removeFirstButton.addActionListener(e -> doRemoveFirst());
        removeLastButton.addActionListener(e -> doRemoveLast());
        removeAtButton.addActionListener(e -> doRemoveAt());
        layoutButton.addActionListener(e -> toggleLayout());
        resetButton.addActionListener(e -> doReset());

        // Lifecycle: until a list exists, only the constructor can be clicked.
        setOperationsEnabled(false);
    }

    /** Every control except the constructor: enabled only while a list exists. */
    private void setOperationsEnabled(boolean enabled) {
        for (JButton b : new JButton[]{addButton, addFirstButton, addAtButton,
                removeFirstButton, removeLastButton, removeAtButton, layoutButton, resetButton}) {
            b.setEnabled(enabled);
        }
        addIndexSpinner.setEnabled(enabled);
        removeIndexSpinner.setEnabled(enabled);
        setLinkRadiosEnabled(enabled && scatterLayout);
    }

    /** Left-hand caption of a button row; a 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(78, 18));
        return label;
    }

    private static JSpinner createIndexSpinner() {
        JSpinner spinner = new JSpinner(new SpinnerNumberModel(0, 0, 99, 1));
        spinner.setToolTipText("index i, read by the button on this row");
        return spinner;
    }

    /**
     * One compound control: an index-taking button together with the "i ="
     * spinner it reads. Wrapping both in one bordered container makes the
     * pairing part of the layout, instead of something the reader has to infer.
     */
    private static JPanel indexGroup(JButton button, JSpinner spinner) {
        JPanel group = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 2));
        group.setBorder(GROUP_BORDER);
        JLabel label = new JLabel("i =");
        label.setFont(new Font(Font.MONOSPACED, Font.BOLD, 13));
        group.add(button);
        group.add(label);
        group.add(spinner);
        return group;
    }

    // ======================================================================
    // Operations
    // ======================================================================

    private void doConstructor() {
        list = new LinkedList<>();
        nextLabel = 0;
        updateAddButtonTexts();
        clearHighlights();
        setOperationsEnabled(true);
        constructorButton.setEnabled(false);
        logLine(String.format("%-26s size=%d  [empty: first = last = null]", "new LinkedList<String>()", 0));
        refreshView();
    }

    private void doAdd() {
        int oldSize = list.size();
        String value = pendingValue();
        list.add(value);
        clearHighlights();
        lastTouchedIndex = oldSize;
        logLine(String.format("%-24s size %-10s  [%s]",
                "add(\"" + value + "\")",
                oldSize + " -> " + list.size(),
                "appended at tail: old last.next = new node, last = new node"));
        advancePendingValue();
        refreshView();
    }

    private void doAddFirst() {
        int oldSize = list.size();
        String value = pendingValue();
        list.addFirst(value);
        clearHighlights();
        lastTouchedIndex = 0;
        logLine(String.format("%-24s size %-10s  [%s]",
                "addFirst(\"" + value + "\")",
                oldSize + " -> " + list.size(),
                "prepended at head: new node.next = old first, first = new node"));
        advancePendingValue();
        refreshView();
    }

    private void doAddAt() {
        int index = indexValue(addIndexSpinner);
        int size = list.size();
        if (index < 0 || index > size) {
            logOutOfRange("add(" + index + ", \"" + pendingValue() + "\")", index, "0.." + size, addIndexGroup);
            return;
        }
        String value = pendingValue();
        String note;
        if (index == size) {
            note = "index == size, so it links at the tail (no walk)";
        } else {
            note = "node(" + index + "): " + walkDescription(index, size) + ", linked before it";
        }
        list.add(index, value);
        clearHighlights();
        lastTouchedIndex = index;
        logLine(String.format("%-24s size %-10s  [%s]",
                "add(" + index + ", \"" + value + "\")",
                size + " -> " + list.size(), note));
        advancePendingValue();
        refreshView();
    }

    private void doRemoveFirst() {
        if (list.isEmpty()) {
            reject("removeFirst()", "removeFirst() would throw NoSuchElementException", null);
            return;
        }
        int oldSize = list.size();
        list.removeFirst();
        clearHighlights();
        logLine(String.format("%-24s size %-10s  [%s]",
                "removeFirst()", oldSize + " -> " + list.size(),
                "first = old first.next; the new first.prev = null"));
        refreshView();
    }

    private void doRemoveLast() {
        if (list.isEmpty()) {
            reject("removeLast()", "removeLast() would throw NoSuchElementException", null);
            return;
        }
        int oldSize = list.size();
        list.removeLast();
        clearHighlights();
        logLine(String.format("%-24s size %-10s  [%s]",
                "removeLast()", oldSize + " -> " + list.size(),
                "last = old last.prev; the new last.next = null"));
        refreshView();
    }

    private void doRemoveAt() {
        int size = list.size();
        int index = indexValue(removeIndexSpinner);
        if (size == 0) {
            reject("remove(" + index + ")", "remove(i) cannot run on an empty list", null);
            return;
        }
        if (index < 0 || index >= size) {
            logOutOfRange("remove(" + index + ")", index, "0.." + (size - 1), removeIndexGroup);
            return;
        }
        String note = "node(" + index + "): " + walkDescription(index, size) + ", unlinked";
        list.remove(index);
        clearHighlights();
        logLine(String.format("%-24s size %-10s  [%s]",
                "remove(" + index + ")", size + " -> " + list.size(), note));
        refreshView();
    }

    private void toggleLayout() {
        scatterLayout = !scatterLayout;
        layoutButton.setText(scatterLayout ? "Show logical chain" : "Show heap scatter");
        setLinkRadiosEnabled(scatterLayout);
        refreshView();
    }

    /** The link-direction radios only make sense while the scatter is visible. */
    private void setLinkRadiosEnabled(boolean enabled) {
        nextLinkRadio.setEnabled(enabled);
        prevLinkRadio.setEnabled(enabled);
    }

    /**
     * Full restart: unlike the constructor button (which only replaces the
     * list), Reset also clears the log and returns every control - layout,
     * radios, spinners - to the initial state.
     */
    private void doReset() {
        list = null;
        nextLabel = 0;
        updateAddButtonTexts();
        addIndexSpinner.setValue(0);
        removeIndexSpinner.setValue(0);
        scatterLayout = false;
        scatterShowPrev = false;
        layoutButton.setText("Show heap scatter");
        nextLinkRadio.setSelected(true);
        setOperationsEnabled(false);
        constructorButton.setEnabled(true);
        clearHighlights();
        logArea.setText("");
        logGuidance();
        refreshView();
    }

    // ======================================================================
    // Walk model: mirrors AbstractSequentialList/LinkedList.node(int)
    // ======================================================================

    /** Explains which side the walk started from and how many nodes it touched. */
    private static String walkDescription(int index, int size) {
        int half = size >> 1;
        boolean fromTail = index >= half;
        int visited = fromTail ? size - index : index + 1;
        return fromTail
                ? index + " >= (size >> 1 = " + half + "): " + visited + " node(s) back from last via prev"
                : index + " < (size >> 1 = " + half + "): " + visited + " node(s) on from first via next";
    }

    // ======================================================================
    // View helpers
    // ======================================================================

    private static int indexValue(JSpinner spinner) {
        return (Integer) spinner.getValue();
    }

    /** Value the next insert will use: A, B, ... Z, A1, B1, ... */
    private String pendingValue() {
        return labelFor(nextLabel);
    }

    /** Consumes the pending value and shows the next one on the three "add" buttons. */
    private void advancePendingValue() {
        nextLabel++;
        updateAddButtonTexts();
    }

    private static String labelFor(int n) {
        char letter = (char) ('A' + n % 26);
        int round = n / 26;
        return round == 0 ? String.valueOf(letter) : letter + String.valueOf(round);
    }

    /** Keeps each "add" button showing the value it would insert. */
    private void updateAddButtonTexts() {
        String value = pendingValue();
        addButton.setText("add(\"" + value + "\")");
        addFirstButton.setText("addFirst(\"" + value + "\")");
        addAtButton.setText("add(i, \"" + value + "\")");
    }

    /**
     * A rejected call changes nothing, so it must not look like a success:
     * it goes to the log, is repeated on the banner, and the control that
     * caused it is outlined in red until the next successful call.
     */
    private void reject(String op, String reason, JPanel offendingControl) {
        logLine(String.format("%-24s rejected: %s", op, reason));
        rejectLabel.setText("rejected   " + op + "   -   " + reason);
        rejectLabel.setOpaque(true);
        rejectLabel.setBackground(REJECT_COLOR);
        rejectLabel.setForeground(Color.WHITE);
        if (offendingControl != null) {
            offendingControl.setBorder(GROUP_REJECT_BORDER);
        }
    }

    private void logOutOfRange(String op, int index, String range, JPanel offendingControl) {
        reject(op, "index " + index + " is out of range (" + range + ")", offendingControl);
    }

    private void clearHighlights() {
        lastTouchedIndex = -1;
        addIndexGroup.setBorder(GROUP_BORDER);
        removeIndexGroup.setBorder(GROUP_BORDER);
        rejectLabel.setText(" ");
        rejectLabel.setOpaque(false);
    }

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

    private void refreshView() {
        if (list == null) {
            sizeLabel.setText("size: -");
            firstLabel.setText("first: -");
            lastLabel.setText("last: -");
            canvas.setData(null);
            return;
        }
        List<NodeView> nodes = snapshot();
        sizeLabel.setText("size: " + nodes.size());
        firstLabel.setText("first: " + describe(nodes, 0));
        lastLabel.setText("last: " + describe(nodes, nodes.size() - 1));
        canvas.setData(nodes);
    }

    private static String describe(List<NodeView> nodes, int index) {
        if (index < 0 || index >= nodes.size()) {
            return "null";
        }
        NodeView node = nodes.get(index);
        return "\"" + node.item() + "\"";
    }

    /** Walks first -> next -> ... and records item plus identity hash per node. */
    private List<NodeView> snapshot() {
        List<NodeView> nodes = new ArrayList<>();
        try {
            int size = (int) sizeField.get(list);
            Object node = firstField.get(list);
            int guard = 0;
            while (node != null && guard++ <= size) {
                nodes.add(new NodeView((String) itemField.get(node),
                        System.identityHashCode(node)));
                node = nextField.get(node);
            }
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
        return nodes;
    }

    /** One node of the chain, as read through reflection. */
    private record NodeView(String item, int id) {
    }

    // ======================================================================
    // Canvas
    // ======================================================================

    private final class ChainView extends JPanel {

        private List<NodeView> nodes = new ArrayList<>();

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

        void setData(List<NodeView> nodes) {
            this.nodes = nodes;
            setPreferredSize(preferredSizeFor(nodes == null ? 0 : nodes.size()));
            revalidate();
            repaint();
        }

        private Dimension preferredSizeFor(int n) {
            if (scatterLayout) {
                int rows = Math.max(MIN_SCATTER_ROWS,
                        (n + SCATTER_COLS - 1) / SCATTER_COLS);
                return new Dimension(SCATTER_COLS * SLOT_W + 24, rows * SLOT_H + 40);
            }
            return new Dimension(Math.max(660, CHAIN_LEFT + n * CHAIN_PITCH + 110), 236);
        }

        @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 (nodes == null) {
                paintNoInstanceMessage(g2);
                return;
            }
            if (nodes.isEmpty()) {
                paintEmptyMessage(g2);
                return;
            }
            if (scatterLayout) {
                paintScattered(g2);
            } else {
                paintChain(g2);
            }
        }

        private void paintNoInstanceMessage(Graphics2D g2) {
            g2.setColor(Color.GRAY);
            g2.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 14));
            g2.drawString("no LinkedList yet - press new LinkedList<String>() to create one", 24, 60);
        }

        private void paintEmptyMessage(Graphics2D g2) {
            g2.setColor(Color.GRAY);
            g2.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 14));
            g2.drawString("empty list: first = last = null, size = 0 - "
                    + "there are no node objects at all", 24, 60);
        }

        // ---------------- logical chain ----------------

        private void paintChain(Graphics2D g2) {
            int n = nodes.size();
            int y = CHAIN_TOP;
            int midY = y + NODE_H / 2;

            // Links: they live inside the gaps, so they never cross a box.
            // The two directions get their own color: next on top, prev below.
            g2.setStroke(LINK_STROKE);
            for (int i = 0; i < n - 1; i++) {
                int x = CHAIN_LEFT + i * CHAIN_PITCH;
                g2.setColor(NEXT_COLOR);
                drawArrow(g2, x + NODE_W, y + 13, x + CHAIN_PITCH, y + 13);       // next
                g2.setColor(PREV_COLOR);
                drawArrow(g2, x + CHAIN_PITCH, y + 27, x + NODE_W, y + 27);       // prev
            }

            // null terminators: first.prev and last.next are null, each drawn in
            // the color of the reference it stands for
            int headX = CHAIN_LEFT;
            int tailX = CHAIN_LEFT + (n - 1) * CHAIN_PITCH + NODE_W;
            g2.setStroke(NULL_STROKE);
            g2.setColor(PREV_COLOR);
            drawArrow(g2, headX - 2, midY, headX - 40, midY);
            g2.setColor(NEXT_COLOR);
            drawArrow(g2, tailX + 2, midY, tailX + 40, midY);
            g2.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11));
            g2.setColor(PREV_COLOR);
            g2.drawString("null", headX - 74, midY + 4);
            g2.setColor(NEXT_COLOR);
            g2.drawString("null", tailX + 44, midY + 4);

            paintFirstLastTags(g2, CHAIN_LEFT, y, n);

            for (int i = 0; i < n; i++) {
                drawNode(g2, CHAIN_LEFT + i * CHAIN_PITCH, y, nodes.get(i), i);
            }

            paintNote(g2, y + NODE_H + 46,
                    "every box is a separate Node object somewhere in the heap; "
                            + "the order exists only in the next/prev links");
            paintLegend(g2, y + NODE_H + 68,
                    "navy arrow (upper) = next, forward   |   magenta arrow (lower) = prev, backward   |   "
                            + "orange = node touched by the last call");
        }

        private void paintFirstLastTags(Graphics2D g2, int leftX, int y, int n) {
            g2.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 11));
            g2.setColor(NODE_COLOR);
            if (n == 1) {
                String tag = "first = last";
                g2.drawString(tag, leftX + (NODE_W - g2.getFontMetrics().stringWidth(tag)) / 2, y - 20);
                return;
            }
            String first = "first";
            String last = "last";
            FontMetrics fm = g2.getFontMetrics();
            int firstCenter = leftX + NODE_W / 2;
            int lastCenter = leftX + (n - 1) * CHAIN_PITCH + NODE_W / 2;
            g2.drawString(first, firstCenter - fm.stringWidth(first) / 2, y - 20);
            g2.drawString(last, lastCenter - fm.stringWidth(last) / 2, y - 20);
            g2.setStroke(new BasicStroke(1.4f));
            drawArrow(g2, firstCenter, y - 15, firstCenter, y - 3);
            drawArrow(g2, lastCenter, y - 15, lastCenter, y - 3);
        }

        // ---------------- scattered layout ----------------

        private void paintScattered(Graphics2D g2) {
            int n = nodes.size();
            int cols = SCATTER_COLS;
            int rows = Math.max(MIN_SCATTER_ROWS, (n + cols - 1) / cols);
            int total = cols * rows;

            paintHeapGrid(g2, cols, rows);

            int[] slots = assignSlots(n, total);
            Rectangle[] boxes = new Rectangle[n];
            for (int i = 0; i < n; i++) {
                boxes[i] = boxInSlot(slots[i]);
            }

            // Links first, then the boxes on top of them.
            // Only one direction at a time - drawing both doubles every line.
            for (int i = 0; i < n - 1; i++) {
                g2.setStroke(LINK_STROKE);
                int bend = (i % 2 == 0) ? 30 : -30;
                if (scatterShowPrev) {
                    g2.setColor(PREV_COLOR);
                    drawCurvedArrow(g2, boxes[i + 1], boxes[i], bend);
                } else {
                    g2.setColor(NEXT_COLOR);
                    drawCurvedArrow(g2, boxes[i], boxes[i + 1], bend);
                }
            }
            for (int i = 0; i < n; i++) {
                drawNode(g2, boxes[i].x, boxes[i].y, nodes.get(i), i);
            }

            g2.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 10));
            g2.setColor(NODE_COLOR);
            labelUnder(g2, boxes[0], "first");
            if (n > 1) {
                labelUnder(g2, boxes[n - 1], "last");
            }

            // Null terminator at the end of the direction being shown:
            // first.prev (magenta) in prev mode, last.next (navy) in next mode.
            if (scatterShowPrev) {
                paintNullStub(g2, boxes[0], true);
            } else {
                paintNullStub(g2, boxes[n - 1], false);
            }

            String linksShown = scatterShowPrev
                    ? "magenta prev links (backward)"
                    : "navy next links (forward)";
            paintNote(g2, rows * SLOT_H + 20,
                    "positions = identity hash (a pseudo \"address\" per node object), not real addresses; "
                            + "only the " + linksShown + " are drawn here");
        }

        /** Short arrow ending in "null"; stays within the slot margin, so it never touches a neighbor box. */
        private void paintNullStub(Graphics2D g2, Rectangle box, boolean leftSide) {
            int y = box.y + box.height / 2;
            int startX = leftSide ? box.x : box.x + box.width;
            int endX = leftSide ? startX - 24 : startX + 24;
            g2.setColor(leftSide ? PREV_COLOR : NEXT_COLOR);
            g2.setStroke(NULL_STROKE);
            drawArrow(g2, startX, y, endX, y);
            g2.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11));
            FontMetrics fm = g2.getFontMetrics();
            g2.drawString("null", Math.max(2, endX - fm.stringWidth("null") / 2), y - 5);
        }

        private void paintHeapGrid(Graphics2D g2, int cols, int rows) {
            g2.setColor(FAINT_COLOR);
            g2.setStroke(new BasicStroke(1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
                    1f, new float[]{3f, 5f}, 0f));
            for (int c = 1; c < cols; c++) {
                int x = c * SLOT_W;
                g2.drawLine(x, 0, x, rows * SLOT_H);
            }
            for (int r = 1; r < rows; r++) {
                int y = r * SLOT_H;
                g2.drawLine(0, y, cols * SLOT_W, y);
            }
        }

        /** Deterministic slot per node: identity hash, with linear probing on collisions. */
        private int[] assignSlots(int n, int total) {
            boolean[] used = new boolean[total];
            int[] slots = new int[n];
            for (int i = 0; i < n; i++) {
                int slot = Math.floorMod(nodes.get(i).id(), total);
                while (used[slot]) {
                    slot = (slot + 1) % total;
                }
                used[slot] = true;
                slots[i] = slot;
            }
            return slots;
        }

        private Rectangle boxInSlot(int slot) {
            int col = slot % SCATTER_COLS;
            int row = slot / SCATTER_COLS;
            return new Rectangle(col * SLOT_W + (SLOT_W - NODE_W) / 2,
                    row * SLOT_H + (SLOT_H - NODE_H) / 2, NODE_W, NODE_H);
        }

        private void labelUnder(Graphics2D g2, Rectangle box, String text) {
            FontMetrics fm = g2.getFontMetrics();
            g2.drawString(text, box.x + (box.width - fm.stringWidth(text)) / 2, box.y + box.height + 15);
        }

        // ---------------- shared drawing ----------------

        private void drawNode(Graphics2D g2, int x, int y, NodeView node, int index) {
            boolean touched = index == lastTouchedIndex;

            g2.setColor(NODE_COLOR);
            g2.fillRoundRect(x, y, NODE_W, NODE_H, 12, 12);

            if (touched) {
                g2.setColor(TOUCHED_COLOR);
                g2.setStroke(new BasicStroke(3f));
            } else {
                g2.setColor(OUTLINE_COLOR);
                g2.setStroke(new BasicStroke(1f));
            }
            g2.drawRoundRect(x, y, NODE_W, NODE_H, 12, 12);

            // item, vertically centered
            g2.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
            g2.setColor(Color.WHITE);
            FontMetrics fm = g2.getFontMetrics();
            String item = "\"" + node.item() + "\"";
            g2.drawString(item, x + (NODE_W - fm.stringWidth(item)) / 2,
                    y + (NODE_H + fm.getAscent() - fm.getDescent()) / 2);
        }

        private void paintNote(Graphics2D g2, int y, String text) {
            g2.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 11));
            g2.setColor(new Color(105, 105, 105));
            g2.drawString(text, 14, y);
        }

        private void paintLegend(Graphics2D g2, int y, String text) {
            g2.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 10));
            g2.setColor(new Color(140, 140, 140));
            g2.drawString(text, 14, y);
        }

        private void drawArrow(Graphics2D g2, int x1, int y1, int x2, int y2) {
            g2.drawLine(x1, y1, x2, y2);
            fillArrowHead(g2, x1, y1, x2, y2);
        }

        private void drawCurvedArrow(Graphics2D g2, Rectangle from, Rectangle to, int bend) {
            Point2D a = borderPoint(from, to.getCenterX(), to.getCenterY());
            Point2D b = borderPoint(to, from.getCenterX(), from.getCenterY());
            double mx = (a.getX() + b.getX()) / 2;
            double my = (a.getY() + b.getY()) / 2;
            double dx = b.getX() - a.getX();
            double dy = b.getY() - a.getY();
            double len = Math.max(1e-6, Math.hypot(dx, dy));
            double cx = mx - dy / len * bend;
            double cy = my + dx / len * bend;
            g2.draw(new QuadCurve2D.Double(a.getX(), a.getY(), cx, cy, b.getX(), b.getY()));
            fillArrowHead(g2, cx, cy, b.getX(), b.getY());
        }

        /** Point where the segment center -> (tx, ty) leaves the rectangle border. */
        private Point2D borderPoint(Rectangle r, double tx, double ty) {
            double cx = r.getCenterX();
            double cy = r.getCenterY();
            double dx = tx - cx;
            double dy = ty - cy;
            double sx = Math.abs(dx) < 1e-9 ? Double.MAX_VALUE : (r.getWidth() / 2.0) / Math.abs(dx);
            double sy = Math.abs(dy) < 1e-9 ? Double.MAX_VALUE : (r.getHeight() / 2.0) / Math.abs(dy);
            double s = Math.min(sx, sy);
            return new Point2D.Double(cx + dx * s, cy + dy * s);
        }

        private void fillArrowHead(Graphics2D g2, double fromX, double fromY, double toX, double toY) {
            double angle = Math.atan2(toY - fromY, toX - fromX);
            double size = 9;
            double left = angle - Math.toRadians(24);
            double right = angle + Math.toRadians(24);
            Path2D head = new Path2D.Double();
            head.moveTo(toX, toY);
            head.lineTo(toX - size * Math.cos(left), toY - size * Math.sin(left));
            head.lineTo(toX - size * Math.cos(right), toY - size * Math.sin(right));
            head.closePath();
            g2.fill(head);
        }
    }

    // ======================================================================
    // Entry point
    // ======================================================================

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            LinkedListBehaviorDemo demo;
            try {
                demo = new LinkedListBehaviorDemo();
            } catch (InaccessibleObjectException e) {
                JOptionPane.showMessageDialog(null,
                        """
                        Reflection access to LinkedList internals was denied.

                        On JDK 9+ start the demo with:
                        java --add-opens=java.base/java.util=ALL-UNNAMED \\
                            LinkedListBehaviorDemo""",
                        "--add-opens required", JOptionPane.ERROR_MESSAGE);
                return;
            } catch (ReflectiveOperationException e) {
                JOptionPane.showMessageDialog(null,
                        "Initialization failed: " + e.getMessage(),
                        "Error", JOptionPane.ERROR_MESSAGE);
                return;
            }
            demo.frame.setVisible(true);
            demo.logGuidance();
        });
    }

    /** Startup hints; repeated after Reset, so a restarted session reads the same. */
    private void logGuidance() {
        logLine("what to observe:");
        logLine("  add / addFirst        -> a new node is linked at the tail / head");
        logLine("  add(i, ...)           -> node(i) is walked first, then two neighbours are relinked");
        logLine("  removeFirst/Last      -> one end is unlinked, null termination moves");
        logLine("  remove(i)             -> same walk as add(i, ...), then the node is unlinked");
        logLine("  \"Show heap scatter\"   -> same list at pseudo heap positions:");
        logLine("                           neighbours in the list are not neighbours in memory");
        logLine("");
    }
}

LinkedList 的类图

如果忽略泛型信息,我们可以为 LinkedList 画出简要类图 ⬇️

如何为 java.util.LinkedList 绘制类图?

我使用了 Java 如何自动生成简单的 PlantUML 类图 一文中提供的代码。关键的命令如下

java 复制代码
javac ClassDiagramGenerator.java
java ClassDiagramGenerator 'java.util.LinkedList'

第二个命令的输出如下

puml 复制代码
@startuml

interface java.util.List
interface java.lang.Cloneable
interface java.lang.Iterable
interface java.util.Queue
interface java.util.SequencedCollection
interface java.io.Serializable
interface java.util.Deque
interface java.util.Collection
abstract java.util.AbstractCollection
abstract java.util.AbstractSequentialList
abstract java.util.AbstractList
class java.util.LinkedList
java.lang.Iterable <|-- java.util.Collection
java.util.Collection <|.. java.util.AbstractCollection
java.util.Collection <|-- java.util.SequencedCollection
java.util.SequencedCollection <|-- java.util.List
java.util.AbstractCollection <|-- java.util.AbstractList
java.util.List <|.. java.util.AbstractList
java.util.AbstractList <|-- java.util.AbstractSequentialList
java.util.Collection <|-- java.util.Queue
java.util.Queue <|-- java.util.Deque
java.util.SequencedCollection <|-- java.util.Deque
java.util.AbstractSequentialList <|-- java.util.LinkedList
java.util.List <|.. java.util.LinkedList
java.util.Deque <|.. java.util.LinkedList
java.lang.Cloneable <|.. java.util.LinkedList
java.io.Serializable <|.. java.util.LinkedList

@enduml

借助 IntelliJ IDEA (Community Edition) 中的 PlantUML 插件,就能将上述 puml 转化为类图。

参考资料

相关推荐
布吉岛的石头2 小时前
Java 程序员第 50 阶段3:数据工程:预训练语料清洗与去重
java·开发语言·人工智能·深度学习·transformer
大文说跨境2 小时前
防关联浏览器的指纹覆盖到什么程度:六类参数的实测与自检方法
java·指纹浏览器·tiktok运营
武子康2 小时前
Vercel Cache Reason:相同 MISS,怎样决定下一步查什么
人工智能·后端·agent
Carol06302 小时前
上肢及肩肘关节
java·服务器·数据库
yliucherish2 小时前
Log4Shell 复现实录:我读 OpenJDK 源码,发现两道防线的默认值竟然相反
java
用户782043279242 小时前
我做了个跑在 Electron main 进程里的 AI Agent 引擎:4 个架构决策和 3 个坑
ai编程
维拉德2 小时前
Java新人第一次接打标需求:从抓包找接口到客户端展示
java
杨文说AI与数字化2 小时前
把模型依赖做成可替换的组件:三层适配 + 一套回归评估
后端
自律懒人3 小时前
英伟达循环模型实测:Dory 1.0-4B 三档循环,10 项基准对比 Qwen3.5
ai编程