为什么枚举类中可以定义抽象方法

引子:一个看似说不通的枚举类型

我们先看一段能正常编译、运行的代码 ⬇️

java 复制代码
public enum Operation {
    PLUS {
        double eval(double x, double y) {
            return x + y;
        }
    },
    MINUS {
        double eval(double x, double y) {
            return x - y;
        }
    },
    TIMES {
        double eval(double x, double y) {
            return x * y;
        }
    },
    DIVIDED_BY {
        double eval(double x, double y) {
            return x / y;
        }
    };

    abstract double eval(double x, double y);

    public static void main(String[] args) {
        System.out.println(PLUS.eval(1, 2));
        System.out.println(PLUS.getClass().getName());
    }
}

请将以上代码保存为 Operation.java。通过执行以下命令可以编译 Operation.java 并运行其中的 main 方法。

bash 复制代码
javac Operation.java
java Operation

输出如下

text 复制代码
3.0
Operation$1

Operation.PLUS.eval(1, 2) 返回 3.0。但似乎有两处说不通:

  1. eval 在枚举类里只有声明、没有实现,实现写到了哪里?
  2. 枚举类不能 new,也不像普通抽象类那样能随手继承,那么实现类在哪里?

JDK 里留了一条线索。Enum.getDeclaringClass() 的注释(我参考的是 JDK 21 中的 Enum.java)写着 ⬇️

这个方法的代码只有几行 ⬇️

java 复制代码
@SuppressWarnings("unchecked")
public final Class<E> getDeclaringClass() {
    Class<?> clazz = getClass();
    Class<?> zuper = clazz.getSuperclass();
    return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
}

也就是说:有些常量的运行时类并不是枚举类本身,而是枚举类的子类 。JLS §8.9.1 说得更直接 ⬇️

The optional class body of an enum constant implicitly defines an anonymous class declaration (§15.9.5) that extends the immediately enclosing enum type.

Operation.java 中各个常量后面那段 { ... } 就表明了每个常量都是匿名子类,抽象方法的实现正写在里面。

背景

我们带着上面两个问题继续往下读。本篇用一个 GUI 工具把"枚举类 → 常量 → 常量的运行时类"三层摆在一起,并逐条对照 getClass() 与 getDeclaringClass(),验证线索、补全规则。

注意事项

  • 完整代码用到了 record 和文本块,需要 JDK 16+(写作本文时,我使用的是 JDK 21)
  • 只用到公开 API,不需要 --add-opens:GUI 代码只用 getClass()、getDeclaringClass()、isAnonymousClass()(以及 getName()、getSimpleName()、getCanonicalName());正文的反射旁证另外用到了 isSealed()、getPermittedSubclasses()、getModifiers()、getDeclaredMethods(),同样不涉及私有成员。getSuperclass() 则只出现在 Enum.getDeclaringClass() 的实现里
  • Operation$1 这类名字是 javac 的实现细节。GUI 的类图下方也有一条提示:工具里显示的是本机 javac 的编号结果,换编译器可能会不同

正文

我借助 trae 生成了可以对照"枚举源码 / 常量 / 运行时类"的 GUI 代码。为了行文连贯,我把完整的代码放到 完整代码 这一小节里了。

如何运行

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

bash 复制代码
javac EnumTypeGuiDemo.java

编译之后,会得到 15 个 class 文件 ⬇️

  • EnumTypeGuiDemo.class
  • EnumTypeGuiDemo$1.class(界面里匿名 DefaultTableModel,与枚举无关)
  • EnumTypeGuiDemo$Sample.class、EnumTypeGuiDemo$FactRenderer.class、EnumTypeGuiDemo$HierarchyPanel.class
  • EnumTypeGuiDemo$Direction.class
  • EnumTypeGuiDemo$Operation.class、EnumTypeGuiDemo$Operation$1.class ~ EnumTypeGuiDemo$Operation$4.class
  • EnumTypeGuiDemo$OrderStatus.class、EnumTypeGuiDemo$OrderStatus$1.class ~ EnumTypeGuiDemo$OrderStatus$3.class

其中 $Operation$1 ~ $4 与 $OrderStatus$1 ~ $3 就是本篇的主角:由常量类体生成的匿名子类。要强调的是,这些名字不是规范的一部分 :上面的清单是本机 javac 的编号结果(按常量的声明顺序 1、2、3...),换一个编译器得到的 class文件的名称也许会不同。本文以及 GUI 工具里展示的所有 $1、$2,都只是"某一种编译器编译出来的样子",阅读时关注的是"存在这些子类",而不是它们具体叫什么。

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

bash 复制代码
java EnumTypeGuiDemo

启动后窗口标题是 enum constant type explorer。也可以不弹窗,直接把界面渲染成 PNG:

bash 复制代码
java EnumTypeGuiDemo --preview preview.png
java EnumTypeGuiDemo --preview preview.png 1 2

后两个可选参数分别是样例下标和常量下标,用来生成文中这种固定状态的截图。上面的命令选中的是样例 1(Operation)的常量 2(TIMES)。

界面一览

窗口分成三栏 ⬇️

  • 左栏 sample (enum type):三个枚举样例;constants (values()):按 values() 的顺序排列,也就是声明顺序
  • 中栏 runtime class hierarchy (computed from getSuperclass()):根框是枚举类,右侧是按运行时类分组的常量
  • 右栏:一行摘要,9 行 check / result,以及 enum source------当前样例的枚举源码,选中常量时加粗并加浅蓝底色,方便对照「源码里有没有类体」

三个样例分别是「全部没有类体」「全部有类体」「部分有类体」,正好覆盖这条规则的所有分支。下面逐个来看。

样例一:所有常量都没有类体

Direction 是最普通的写法 ⬇️

java 复制代码
enum Direction {
    EAST, WEST, SOUTH, NORTH
}

选中 EAST,右栏九行 ⬇️

check result
name() EAST
getClass().getName() EnumTypeGuiDemo$Direction
getClass().getSimpleName() Direction
getClass().getCanonicalName() EnumTypeGuiDemo.Direction
getClass().isAnonymousClass() false
getDeclaringClass().getName() EnumTypeGuiDemo$Direction
has class body no
getClass() == E.class ✔ true
getDeclaringClass() == E.class ✔ true

getClass() 与 getDeclaringClass() 完全一致。类图里只有一个根框,下方用一行绿字说明 ⬇️

text 复制代码
every constant is a plain instance of Direction - no subclasses were generated

补一条反射层面的旁证:Direction.class.isSealed() 返回 false------没有常量带类体,枚举类也就没有「隐含的密封子类」。

样例二:每个常量都有类体

Operation 的四个常量都带类体,各自实现 eval。截图里选中的是 TIMES ⬇️

摘要行读作:4 个常量、4 个带类体、5 个 class 文件(枚举类 1 个 + 匿名子类 4 个)。

右栏九行(以 TIMES 为例)⬇️

check result
name() TIMES
getClass().getName() EnumTypeGuiDemo$Operation$3
getClass().getSimpleName() (empty string)
getClass().getCanonicalName() null (anonymous class)
getClass().isAnonymousClass() true
getDeclaringClass().getName() EnumTypeGuiDemo$Operation
has class body yes
getClass() == E.class ✘ false
getDeclaringClass() == E.class ✔ true

getClass() 指向 Operation$3,getDeclaringClass() 却回到 Operation。这正好对应 Enum.getDeclaringClass() 那段实现:如果父类不是 java.lang.Enum,就取父类。

javap -p 能看到对应的结构(-p 让私有构造器也显示出来)⬇️

text 复制代码
final class EnumTypeGuiDemo$Operation$3 extends EnumTypeGuiDemo$Operation {
  private EnumTypeGuiDemo$Operation$3(java.lang.String, int);
  double eval(double, double);
}

而枚举类本身被编译成了抽象类 ⬇️

text 复制代码
abstract class EnumTypeGuiDemo$Operation extends java.lang.Enum<EnumTypeGuiDemo$Operation> {
  public static final EnumTypeGuiDemo$Operation PLUS;
  public static final EnumTypeGuiDemo$Operation MINUS;
  public static final EnumTypeGuiDemo$Operation TIMES;
  public static final EnumTypeGuiDemo$Operation DIVIDED_BY;
  ...
  abstract double eval(double, double);
}

abstract 这个修饰符也能用反射验证:Modifier.isAbstract(Operation.class.getModifiers()) 返回 true。

如果有常量漏了实现,编译直接失败。把 MINUS 改成没有类体 ⬇️

java 复制代码
enum Operation {
    PLUS { double eval(double x, double y) { return x + y; } },
    MINUS;

    abstract double eval(double x, double y);
}

javac 的报错会指向 MINUS(JDK 21,中文语言环境)⬇️

text 复制代码
Operation是抽象的; 无法实例化
        MINUS;

JLS §8.9.2 的原话是:

It is a compile-time error if an enum declaration E has an abstract method m as a member, unless E has at least one enum constant and all of E's enum constants have class bodies that provide concrete implementations of m.

样例三:只有部分常量有类体

OrderStatus 是更接近业务的写法:只有终态需要自己的行为,另外还留了一个空类体的常量 ⬇️

java 复制代码
enum OrderStatus {
    CREATED,
    PAID,
    SHIPPED,
    // { } alone still produces a subclass
    ON_HOLD { },
    COMPLETED {
        @Override
        boolean isTerminal() { return true; }
    },
    CANCELLED {
        @Override
        boolean isTerminal() { return true; }
    };

    boolean isTerminal() { return false; }
}

界面把三类常量都放进同一张图 ⬇️

  • 根框 OrderStatus 挂着 CREATED, PAID, SHIPPED,它们的 getClass() 就是枚举类
  • ON_HOLD { }、COMPLETED、CANCELLED 各占一个匿名子类框:OrderStatus$1、$2、$3

选中 ON_HOLD,右栏的 has class body 是 yes,getClass() == E.class 是 ✘ false。虽然这个类体里什么都没有,javap -p 里也确实只有一个构造器 ⬇️

text 复制代码
final class EnumTypeGuiDemo$OrderStatus$1 extends EnumTypeGuiDemo$OrderStatus {
  private EnumTypeGuiDemo$OrderStatus$1(java.lang.String, int);
}

反射也能印证:OrderStatus$1 的 getDeclaredMethods().length 是 0,而实现了 isTerminal() 的 OrderStatus$2 是 1 ⬇️

text 复制代码
EnumTypeGuiDemo$OrderStatus$1 declared methods = 0 -> []
EnumTypeGuiDemo$OrderStatus$2 declared methods = 1 -> [boolean EnumTypeGuiDemo$OrderStatus$2.isTerminal()]

也就是说,空类体什么方法都没写,但「它有自己的类」这件事已经发生了 。getClass() 从此不再等于枚举类。

选中终态 COMPLETED 时,类图高亮的是 $2 ⬇️

值得注意的是,getDeclaringClass() 在这六种情况下都返回 OrderStatus,检查表最后一行始终是 ✔ true。

再补一条:只要有一个常量带类体,枚举类就是隐含 sealed 的。OrderStatus.class.isSealed() 返回 true,getPermittedSubclasses() 正好列出三个匿名子类 ⬇️

text 复制代码
[class EnumTypeGuiDemo$OrderStatus$1, class EnumTypeGuiDemo$OrderStatus$2, class EnumTypeGuiDemo$OrderStatus$3]

JLS §8.9 的说法是:

An enum class E is implicitly sealed if its declaration contains at least one enum constant that has a class body. The permitted direct subclasses of E are the anonymous classes implicitly declared by the enum constants that have a class body.

为什么枚举类中可以定义抽象方法

把上面三个样例汇总,规则可以拆成三条:

  1. 抽象方法的例外 (JLS §8.4.3):普通类里,抽象方法必须写在抽象类中;JLS 为枚举声明单独开了例外------ The declaration of an abstract method m must appear directly within an abstract class (call it A) unless it occurs within an enum declaration.
  2. 常量类体就是匿名子类 (JLS §8.9.1): The optional class body of an enum constant implicitly declares an anonymous class that is a direct subclass of the immediately enclosing enum class, and is final.
  3. 必须全部有实现 (JLS §8.9.2): It is a compile-time error if an enum declaration E has an abstract method m as a member, unless E has at least one enum constant and all of E's enum constants have class bodies that provide concrete implementations of m.

合起来就是:抽象方法由「常量的匿名子类」实现,枚举类自己不需要实现,所以编译器允许这组声明存在。

编译器具体做了三件事,javap -c 里的 static {} 可以看到 ⬇️

text 复制代码
static {};
  Code:
     0: new           #35                 // class EnumTypeGuiDemo$Operation$1
     3: dup
     4: ldc           #37                 // String PLUS
     6: iconst_0
     7: invokespecial #38                 // Method EnumTypeGuiDemo$Operation$1."<init>":(Ljava/lang/String;I)V
    10: putstatic     #3                  // Field PLUS:LEnumTypeGuiDemo$Operation;
    13: new           #39                 // class EnumTypeGuiDemo$Operation$2
    16: dup
    17: ldc           #41                 // String MINUS
    19: iconst_1
    20: invokespecial #42                 // Method EnumTypeGuiDemo$Operation$2."<init>":(Ljava/lang/String;I)V
    23: putstatic     #7                  // Field MINUS:LEnumTypeGuiDemo$Operation;
    ...
  1. 声明了抽象方法的枚举类被标记为 abstract(OrderStatus 没有抽象方法,就没有这个修饰符,Modifier.isAbstract 分别返回 true / false)
  2. 每个带类体的常量,new 的是对应的匿名子类;("PLUS", 0) 里的 0 就是 ordinal
  3. 所有实例放进 $VALUES,values() 再对外返回

所以 Operation.PLUS.eval(1, 2) 的真正接收者是 Operation$1,而不是 Operation。静态字段 PLUS 的类型仍然是 Operation,多态在这里照常生效。

这条规则的连带影响

1. getClass() 比较运行时类型,getDeclaringClass() 比较枚举类型

java 复制代码
Operation.PLUS.getClass() == Operation.MINUS.getClass();   // false
Operation.PLUS.getDeclaringClass() == Operation.class;     // true

JDK 自己的 Enum.compareTo 就是这么写的 ⬇️

java 复制代码
public final int compareTo(E o) {
    Enum<?> other = o;
    Enum<E> self = this;
    if (self.getClass() != other.getClass() && // optimization
        self.getDeclaringClass() != other.getDeclaringClass())
        throw new ClassCastException();
    return self.ordinal - other.ordinal;
}

先用 getClass() 做一次快速判断(大多数枚举没有类体,这一步就已经相等),不相等再退回 getDeclaringClass()。

2. 匿名类的元信息是空的

getSimpleName() 返回空串,getCanonicalName() 返回 null,isAnonymousClass() 返回 true。更彻底的一点:OrderStatus$1.isEnum() 是 false、getEnumConstants() 是 null------这个常量的运行时类甚至不算枚举类。判断「它属于哪个枚举」只能靠 getDeclaringClass() 或 ordinal()。

3. 集合类与 switch 不受影响

EnumSet、EnumMap、switch 都基于 ordinal() 或常量本身,实测不受影响 ⬇️

text 复制代码
COMPLETED.compareTo(ON_HOLD) = 1
EnumMap keys = [ON_HOLD, COMPLETED], size = 2
EnumSet = [ON_HOLD, COMPLETED]

4. 不要依赖 $1 这种命名

Operation$1 ~ $4 的顺序取决于常量的声明顺序和 javac 的实现。JLS §13.1 明确说匿名类的二进制名不可预测,换编译器或调整声明顺序都可能变。

5. 带类体的枚举是隐含 sealed

这一点在样例三里已经看到:isSealed() 为 true,允许的子类就是那些匿名子类。它解释了一个直觉------常量的匿名子类不会被别的代码继承。

完整代码

java 复制代码
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextPane;
import javax.swing.JToggleButton;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;

/**
 * GUI explorer for the runtime type of enum constants: a constant declared
 * with a class body is an instance of an anonymous subclass (E$1), so its
 * getClass() differs from the enum type while getDeclaringClass() does not.
 * <p>
 * Note: binary names such as E$1 are NOT part of the spec. JLS 13.1 says the
 * class file name of an anonymous class is implementation determined, so the
 * names shown here are whatever the local compiler produced (javac numbers
 * them in declaration order) and may differ across compilers.
 * <p>
 * Run with:
 * javac EnumTypeGuiDemo.java
 * java EnumTypeGuiDemo
 * <p>
 * Render the layout to an image without opening a window:
 * java EnumTypeGuiDemo --preview preview.png
 */
public class EnumTypeGuiDemo {

    /** Plain enum: every constant is an instance of the enum type itself. */
    enum Direction {
        EAST, WEST, SOUTH, NORTH
    }

    /** Every constant has a class body, so each one is an anonymous subclass. */
    enum Operation {
        PLUS {
            double eval(double x, double y) {
                return x + y;
            }
        },
        MINUS {
            double eval(double x, double y) {
                return x - y;
            }
        },
        TIMES {
            double eval(double x, double y) {
                return x * y;
            }
        },
        DIVIDED_BY {
            double eval(double x, double y) {
                return x / y;
            }
        };

        // Each constant supports an arithmetic operation
        abstract double eval(double x, double y);
    }

    /**
     * A realistic mix: only the terminal states need their own behaviour, and
     * one constant keeps an empty class body to show that even {@code { }} is
     * enough to produce a separate subclass.
     */
    enum OrderStatus {
        CREATED,
        PAID,
        SHIPPED,
        // { } alone still produces a subclass
        ON_HOLD { },
        COMPLETED {
            @Override
            boolean isTerminal() {
                return true;
            }
        },
        CANCELLED {
            @Override
            boolean isTerminal() {
                return true;
            }
        };

        boolean isTerminal() {
            return false;
        }
    }

    /** One selectable enum type plus the source readers can compare with. */
    record Sample(String label, Class<?> type, Enum<?>[] constants, String source) {
    }

    private static final String DIRECTION_SOURCE = """
            enum Direction {
                EAST, WEST, SOUTH, NORTH
            }
            """;

    private static final String OPERATION_SOURCE = """
            enum Operation {
                PLUS {
                    double eval(double x, double y) { return x + y; }
                },
                MINUS {
                    double eval(double x, double y) { return x - y; }
                },
                TIMES {
                    double eval(double x, double y) { return x * y; }
                },
                DIVIDED_BY {
                    double eval(double x, double y) { return x / y; }
                };

                abstract double eval(double x, double y);
            }
            """;

    private static final String ORDER_STATUS_SOURCE = """
            enum OrderStatus {
                CREATED,
                PAID,
                SHIPPED,
                // { } alone still produces a subclass
                ON_HOLD { },
                COMPLETED {
                    @Override
                    boolean isTerminal() { return true; }
                },
                CANCELLED {
                    @Override
                    boolean isTerminal() { return true; }
                };

                boolean isTerminal() { return false; }
            }
            """;

    private final List<Sample> samples = List.of(
            new Sample("Direction (plain)", Direction.class, Direction.values(), DIRECTION_SOURCE),
            new Sample("Operation (all bodies)", Operation.class, Operation.values(), OPERATION_SOURCE),
            new Sample("OrderStatus (some bodies)", OrderStatus.class, OrderStatus.values(),
                    ORDER_STATUS_SOURCE));

    private final HierarchyPanel diagram = new HierarchyPanel();
    private final DefaultListModel<String> constantModel = new DefaultListModel<>();
    private final JList<String> constantList = new JList<>(constantModel);
    private final JLabel header = new JLabel(" ");
    private final DefaultTableModel facts = new DefaultTableModel(
            new Object[]{"check", "result"}, 0) {
        @Override
        public boolean isCellEditable(int row, int column) {
            return false;
        }
    };
    private final JTable factTable = new JTable(facts);
    private final JScrollPane factScroll = new JScrollPane(factTable);
    private final JTextPane sourcePane = new JTextPane();
    private final JScrollPane sourceScroll = new JScrollPane(sourcePane);
    private final JPanel sourceBox = new JPanel(new BorderLayout());
    private final List<JToggleButton> sampleButtons = new ArrayList<>();

    private Sample current;

    public static void main(String[] args) throws IOException {
        if (args.length >= 2 && args[0].equals("--preview")) {
            writePreview(args);
            return;
        }
        SwingUtilities.invokeLater(() -> {
            useSystemLookAndFeel();
            new EnumTypeGuiDemo().show();
        });
    }

    private static void useSystemLookAndFeel() {
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (Exception ignored) {
            // keep the default look and feel
        }
    }

    private void show() {
        JFrame frame = new JFrame("enum constant type explorer");
        frame.setContentPane(buildPanel());
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setSize(1320, 700);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    // ---- layout: left samples + constants, center diagram, right facts ----

    private JPanel buildPanel() {
        JPanel left = buildLeftPanel();
        JPanel center = buildCenterPanel();
        JPanel right = buildRightPanel();

        JPanel content = new JPanel(new BorderLayout(8, 0));
        content.add(left, BorderLayout.WEST);
        content.add(center, BorderLayout.CENTER);
        content.add(right, BorderLayout.EAST);

        JPanel panel = new JPanel(new BorderLayout());
        panel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
        panel.setBackground(Color.WHITE);
        panel.add(content, BorderLayout.CENTER);

        selectSample(samples.get(0));
        return panel;
    }

    private JPanel buildLeftPanel() {
        JPanel sampleBox = new JPanel();
        sampleBox.setLayout(new BoxLayout(sampleBox, BoxLayout.Y_AXIS));
        sampleBox.setBorder(BorderFactory.createTitledBorder("sample (enum type)"));
        sampleBox.setBackground(Color.WHITE);

        ButtonGroup group = new ButtonGroup();
        for (Sample sample : samples) {
            JToggleButton button = new JToggleButton(sample.label());
            button.setAlignmentX(Component.LEFT_ALIGNMENT);
            button.setMaximumSize(new Dimension(Integer.MAX_VALUE, 28));
            button.setBackground(Color.WHITE);
            button.setToolTipText("switch to this enum type");
            button.addActionListener(e -> selectSample(sample));
            group.add(button);
            sampleButtons.add(button);
            sampleBox.add(button);
            sampleBox.add(Box.createVerticalStrut(4));
        }

        constantList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        constantList.setToolTipText("select a constant to inspect its runtime class");
        constantList.addListSelectionListener(e -> {
            if (!e.getValueIsAdjusting()) {
                updateSelected();
            }
        });
        JPanel constantBox = new JPanel(new BorderLayout());
        constantBox.setBorder(BorderFactory.createTitledBorder("constants (values())"));
        constantBox.setBackground(Color.WHITE);
        constantBox.add(new JScrollPane(constantList), BorderLayout.CENTER);

        JPanel left = new JPanel(new BorderLayout(0, 8));
        left.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 8));
        left.setBackground(Color.WHITE);
        left.add(sampleBox, BorderLayout.NORTH);
        left.add(constantBox, BorderLayout.CENTER);
        left.setPreferredSize(new Dimension(230, 400));
        return left;
    }

    /** The whole center column belongs to the hierarchy diagram. */
    private JPanel buildCenterPanel() {
        JPanel hierarchy = new JPanel(new BorderLayout(0, 6));
        hierarchy.setBackground(Color.WHITE);
        hierarchy.setBorder(BorderFactory.createTitledBorder(
                "runtime class hierarchy (computed from getSuperclass())"));
        hierarchy.add(diagram, BorderLayout.CENTER);

        // legend + compiler note live here as real components (below the
        // diagram, not painted inside it), so a tall enum can never overlap them
        JPanel noteStrip = new JPanel();
        noteStrip.setLayout(new BoxLayout(noteStrip, BoxLayout.Y_AXIS));
        noteStrip.setBackground(Color.WHITE);
        noteStrip.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, new Color(0xe8, 0xdf, 0xcc)));

        JLabel legendLabel = new JLabel("blue border = runtime class of the selected constant");
        legendLabel.setFont(legendLabel.getFont().deriveFont(Font.PLAIN, 11f));
        legendLabel.setForeground(new Color(0x8c, 0x8c, 0x8c));
        legendLabel.setBorder(BorderFactory.createEmptyBorder(5, 10, 2, 10));
        legendLabel.setAlignmentX(Component.LEFT_ALIGNMENT);

        // the $1 names come from javac's numbering, not from the spec: JLS 13.1
        // leaves the class file name of an anonymous class implementation-determined
        JLabel compilerNote = new JLabel("<html><div style=\"width:560px\">"
                + "<b>class names like Operation$1 are compiler-specific:</b> shown here is"
                + " javac's numbering (JLS 13.1 leaves anonymous class names implementation"
                + " determined); another compiler may name them differently</div></html>");
        compilerNote.setFont(compilerNote.getFont().deriveFont(Font.PLAIN, 11f));
        compilerNote.setForeground(new Color(0x8a, 0x6d, 0x3b));
        compilerNote.setBorder(BorderFactory.createEmptyBorder(2, 10, 5, 10));
        compilerNote.setAlignmentX(Component.LEFT_ALIGNMENT);
        compilerNote.setToolTipText("JLS 13.1: the binary name of an anonymous class"
                + " is implementation determined");
        noteStrip.add(legendLabel);
        noteStrip.add(compilerNote);
        hierarchy.add(noteStrip, BorderLayout.SOUTH);
        return hierarchy;
    }

    private JPanel buildRightPanel() {
        factTable.setRowHeight(24);
        factTable.setShowGrid(true);
        factTable.setGridColor(new Color(0xee, 0xee, 0xee));
        factTable.getColumnModel().getColumn(0).setPreferredWidth(205);
        factTable.getColumnModel().getColumn(1).setPreferredWidth(265);
        factTable.setDefaultRenderer(Object.class, new FactRenderer());

        sourcePane.setEditable(false);
        sourcePane.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
        sourcePane.setBackground(Color.WHITE);
        sourcePane.setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 8));
        sourceBox.setBackground(Color.WHITE);
        sourceBox.setBorder(BorderFactory.createTitledBorder("enum source"));
        sourceBox.add(sourceScroll, BorderLayout.CENTER);

        JPanel right = new JPanel(new BorderLayout(0, 6));
        right.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 0));
        right.setBackground(Color.WHITE);
        right.add(header, BorderLayout.NORTH);
        right.add(factScroll, BorderLayout.CENTER);
        // SOUTH keeps its preferred height, so the source panel shows its whole
        // text when the window allows it and never competes with the diagram
        right.add(sourceBox, BorderLayout.SOUTH);
        right.setPreferredSize(new Dimension(540, 400));
        return right;
    }

    // ---- data flow -------------------------------------------------------

    private void selectSample(Sample sample) {
        current = sample;
        for (int i = 0; i < sampleButtons.size(); i++) {
            sampleButtons.get(i).setSelected(samples.get(i) == sample);
        }
        constantModel.clear();
        for (Enum<?> constant : sample.constants()) {
            constantModel.addElement(constant.name());
        }
        constantList.setSelectedIndex(0);
    }

    private void updateSelected() {
        int index = constantList.getSelectedIndex();
        if (current == null || index < 0) {
            return;
        }
        Enum<?> constant = current.constants()[index];
        Class<?> type = current.type();
        Class<?> runtime = constant.getClass();
        Class<?> declaring = constant.getDeclaringClass();

        int bodies = countBodies();
        header.setText(String.format("%s: %d constants, %d with class body, %d class file%s",
                shortName(type), current.constants().length, bodies,
                1 + (bodies == 0 ? 0 : childCount()), bodies == 1 ? "" : "s"));

        facts.setRowCount(0);
        addFact("name()", constant.name());
        addFact("getClass().getName()", runtime.getName());
        addFact("getClass().getSimpleName()",
                runtime.getSimpleName().isEmpty() ? "(empty string)" : runtime.getSimpleName());
        addFact("getClass().getCanonicalName()",
                runtime.getCanonicalName() == null ? "null (anonymous class)" : runtime.getCanonicalName());
        addFact("getClass().isAnonymousClass()", String.valueOf(runtime.isAnonymousClass()));
        addFact("getDeclaringClass().getName()", declaring.getName());
        addFact("has class body", runtime != type ? "yes" : "no");
        addFact("getClass() == E.class", runtime == type ? "✔ true" : "✘ false");
        addFact("getDeclaringClass() == E.class", declaring == type ? "✔ true" : "✘ false");

        diagram.refresh(current, constant);
        showSource(constant);
    }

    /** Show the enum source of the current sample, highlighting the constant. */
    private void showSource(Enum<?> selected) {
        String text = current.source();
        sourcePane.setText(text);
        if (selected != null) {
            StyledDocument doc = sourcePane.getStyledDocument();
            SimpleAttributeSet attributes = new SimpleAttributeSet();
            StyleConstants.setBold(attributes, true);
            StyleConstants.setBackground(attributes, new Color(0xe6, 0xf4, 0xff));
            Matcher matcher = Pattern.compile("\\b" + Pattern.quote(selected.name()) + "\\b")
                    .matcher(text);
            while (matcher.find()) {
                doc.setCharacterAttributes(matcher.start(),
                        matcher.end() - matcher.start(), attributes, false);
            }
        }
        sourcePane.setCaretPosition(0);

        // SOUTH of the right column: follow the content height, the fact table
        // in CENTER absorbs whatever space is left or scrolls when the window
        // is short
        int lineHeight = sourcePane.getFontMetrics(sourcePane.getFont()).getHeight();
        int lines = text.split("\n", -1).length;
        sourceBox.setPreferredSize(new Dimension(0, lines * lineHeight + 8 + 30));
        sourceBox.revalidate();
    }

    private void addFact(String label, String value) {
        facts.addRow(new Object[]{label, value});
    }

    private int countBodies() {
        int count = 0;
        for (Enum<?> constant : current.constants()) {
            if (constant.getClass() != current.type()) {
                count++;
            }
        }
        return count;
    }

    private int childCount() {
        Map<Class<?>, Boolean> children = new LinkedHashMap<>();
        for (Enum<?> constant : current.constants()) {
            if (constant.getClass() != current.type()) {
                children.put(constant.getClass(), Boolean.TRUE);
            }
        }
        return children.size();
    }

    /** EnumTypeGuiDemo$Operation$1 -> Operation$1 */
    private static String shortName(Class<?> type) {
        String name = type.getName();
        int dollar = name.indexOf('$');
        return dollar >= 0 ? name.substring(dollar + 1) : name.substring(name.lastIndexOf('.') + 1);
    }

    private static String joinNames(List<Enum<?>> constants) {
        List<String> names = new ArrayList<>();
        for (Enum<?> constant : constants) {
            names.add(constant.name());
        }
        return String.join(", ", names);
    }

    // ---- right panel renderer -------------------------------------------

    private static final class FactRenderer extends DefaultTableCellRenderer {

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value,
                boolean isSelected, boolean hasFocus, int row, int column) {
            Component component = super.getTableCellRendererComponent(
                    table, value, isSelected, hasFocus, row, column);
            String text = String.valueOf(value);
            if (!isSelected) {
                if (text.startsWith("✔")) {
                    component.setForeground(new Color(0x1a, 0x7f, 0x37));
                } else if (text.startsWith("✘")) {
                    component.setForeground(new Color(0xcf, 0x22, 0x2e));
                } else if (column == 0) {
                    component.setForeground(new Color(0x59, 0x59, 0x59));
                } else {
                    component.setForeground(new Color(0x1f, 0x1f, 0x1f));
                }
            }
            return component;
        }
    }

    // ---- center panel: class hierarchy ----------------------------------

    private static final class HierarchyPanel extends JPanel {

        private Sample sample;
        private Enum<?> selected;

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

        void refresh(Sample sample, Enum<?> selected) {
            this.sample = sample;
            this.selected = selected;
            repaint();
        }

        @Override
        protected void paintComponent(Graphics graphics) {
            super.paintComponent(graphics);
            Graphics2D g = (Graphics2D) graphics.create();
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);
            try {
                paint(g);
            } finally {
                g.dispose();
            }
        }

        private void paint(Graphics2D g) {
            if (sample == null) {
                return;
            }
            int width = getWidth();
            int height = getHeight();
            int pad = 18;

            List<Enum<?>> plain = new ArrayList<>();
            Map<Class<?>, List<Enum<?>>> children = new LinkedHashMap<>();
            for (Enum<?> constant : sample.constants()) {
                Class<?> runtime = constant.getClass();
                if (runtime == sample.type()) {
                    plain.add(constant);
                } else {
                    children.computeIfAbsent(runtime, key -> new ArrayList<>()).add(constant);
                }
            }

            int rootW = 240;
            int rootH = 66;
            int rootX = pad;
            boolean rootSelected = selected != null && selected.getClass() == sample.type();
            String rootNote = plain.isEmpty()
                    ? "(every constant has a class body)"
                    : "constants: " + joinNames(plain);

            int count = children.size();
            if (count == 0) {
                int rootY = Math.max(30, (height - rootH) / 2 - 14);
                drawBox(g, rootX, rootY, rootW, rootH, shortName(sample.type()),
                        "enum type (E.class)", rootNote, rootSelected);
                g.setColor(new Color(0x1a, 0x7f, 0x37));
                g.setFont(new Font("Dialog", Font.PLAIN, 12));
                drawCentered(g, "every constant is a plain instance of "
                        + shortName(sample.type()) + " - no subclasses were generated", width,
                        rootY + rootH + 56);
                return;
            }

            // root on the left, anonymous subclasses stacked on the right
            int childX = rootX + rootW + 2 * pad;
            int childW = Math.max(150, width - childX - pad);
            int gapY = 10;
            // shrink the boxes (never below 48px, the 3-line minimum) when the
            // panel is short, so the block always fits above the note strip
            int childH = Math.max(48, Math.min(62,
                    (height - 16 - (count - 1) * gapY) / count));
            int blockH = count * childH + (count - 1) * gapY;
            // center the block; never force a 24px top margin when the panel is
            // too short, or the last box would run into the note strip below
            int startY = Math.max(8, (height - blockH) / 2);
            int rootY = startY + blockH / 2 - rootH / 2;

            drawBox(g, rootX, rootY, rootW, rootH, shortName(sample.type()),
                    "enum type (E.class)", rootNote, rootSelected);

            int rootCy = rootY + rootH / 2;
            int i = 0;
            for (Map.Entry<Class<?>, List<Enum<?>>> entry : children.entrySet()) {
                Class<?> runtime = entry.getKey();
                int cy = startY + i * (childH + gapY) + childH / 2;

                g.setColor(new Color(0xbf, 0xbf, 0xbf));
                g.setStroke(new BasicStroke(1f));
                g.drawLine(rootX + rootW, rootCy, childX, cy);
                g.drawLine(childX - 7, cy - 4, childX, cy);
                g.drawLine(childX - 7, cy + 4, childX, cy);

                boolean highlighted = selected != null && selected.getClass() == runtime;
                drawBox(g, childX, cy - childH / 2, childW, childH,
                        shortName(runtime),
                        runtime.isAnonymousClass() ? "anonymous subclass" : "subclass",
                        "constants: " + joinNames(entry.getValue()), highlighted);
                i++;
            }
        }

        private void drawCentered(Graphics2D g, String text, int width, int y) {
            FontMetrics metrics = g.getFontMetrics();
            g.drawString(text, (width - metrics.stringWidth(text)) / 2, y);
        }

        private void drawBox(Graphics2D g, int x, int y, int w, int h,
                String title, String subtitle, String constants, boolean highlighted) {
            g.setColor(highlighted ? new Color(0xe6, 0xf4, 0xff) : Color.WHITE);
            g.fillRoundRect(x, y, w, h, 8, 8);
            g.setColor(highlighted ? new Color(0x16, 0x77, 0xff) : new Color(0xbf, 0xbf, 0xbf));
            g.setStroke(new BasicStroke(highlighted ? 2f : 1f));
            g.drawRoundRect(x, y, w, h, 8, 8);
            g.setStroke(new BasicStroke(1f));

            // text baselines scale with the box height (the layout was designed
            // for h = 62), so a shrunken box keeps all three lines inside
            g.setColor(new Color(0x1f, 0x1f, 0x1f));
            g.setFont(new Font("Dialog", Font.BOLD, 13));
            g.drawString(clip(g, title, w - 20), x + 10, y + h * 21 / 62);

            g.setColor(new Color(0x59, 0x59, 0x59));
            g.setFont(new Font("Dialog", Font.PLAIN, 11));
            g.drawString(clip(g, subtitle, w - 20), x + 10, y + h * 40 / 62);
            g.drawString(clip(g, constants, w - 20), x + 10, y + h * 58 / 62);
        }

        private String clip(Graphics2D g, String text, int maxWidth) {
            FontMetrics metrics = g.getFontMetrics();
            if (metrics.stringWidth(text) <= maxWidth) {
                return text;
            }
            String ellipsis = "...";
            int end = text.length();
            while (end > 0 && metrics.stringWidth(text.substring(0, end) + ellipsis) > maxWidth) {
                end--;
            }
            return text.substring(0, end) + ellipsis;
        }
    }

    // ---- preview: paint the same panel into a PNG -----------------------

    private static void writePreview(String[] args) throws IOException {
        useSystemLookAndFeel();
        EnumTypeGuiDemo demo = new EnumTypeGuiDemo();
        JPanel panel = demo.buildPanel();
        if (args.length >= 3 && Integer.parseInt(args[2]) != 0) {
            demo.selectSample(demo.samples.get(Integer.parseInt(args[2])));
        }
        if (args.length >= 4) {
            demo.constantList.setSelectedIndex(Integer.parseInt(args[3]));
        }
        Path path = Path.of(args[1]);
        int width = 1320;
        int height = 700;
        // offscreen: JTable installs its header into the enclosing JScrollPane
        // in addNotify(), which never runs without a window
        demo.factScroll.setColumnHeaderView(demo.factTable.getTableHeader());
        demo.factScroll.getColumnHeader().setPreferredSize(new Dimension(10,
                demo.factTable.getTableHeader().getPreferredSize().height));
        panel.setSize(new Dimension(width, height));
        layoutAll(panel);

        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = image.createGraphics();
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, width, height);
        panel.printAll(g);
        g.dispose();
        ImageIO.write(image, "png", path.toFile());
        System.out.println("wrote " + path);
    }

    private static void layoutAll(Container container) {
        container.doLayout();
        for (Component child : container.getComponents()) {
            if (child instanceof Container nested) {
                layoutAll(nested);
            }
        }
    }
}
相关推荐
陈随易38 分钟前
Vue 编译器换掉 source map,快了三四成
前端·后端·程序员
文丑颜不良啊2 小时前
LeetCode1941~1960题目解析
java·leetcode
txxzjmzlh2 小时前
SpringBoot 统⼀功能处理
java·spring boot·后端
nice_lcj5204 小时前
Java Lambda 表达式入门:怎么写、怎么用、常用方法有哪些
java·开发语言·后端·面试
码农小河664 小时前
基于SSM+JSP+MySQL的在线医疗服务系统的设计与实现
java·开发语言·mysql
IT_陈寒5 小时前
Redis大key删除为什么会导致服务卡顿?我的线上事故复盘
前端·人工智能·后端
系统集成-鹰翔5 小时前
ERP图片改名后仍命中识别缓存:缓存键、十分钟到期与调用计数
javascript·经验分享·后端
用户8356290780515 小时前
使用 Python 在 Excel 中添加和自定义迷你图
后端·python
码事漫谈5 小时前
给AI看一颗❤️,它就开始写递归了
后端