引子:一个看似说不通的枚举类型
我们先看一段能正常编译、运行的代码 ⬇️
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。但似乎有两处说不通:
eval在枚举类里只有声明、没有实现,实现写到了哪里?- 枚举类不能
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.classEnumTypeGuiDemo$1.class(界面里匿名DefaultTableModel,与枚举无关)EnumTypeGuiDemo$Sample.class、EnumTypeGuiDemo$FactRenderer.class、EnumTypeGuiDemo$HierarchyPanel.classEnumTypeGuiDemo$Direction.classEnumTypeGuiDemo$Operation.class、EnumTypeGuiDemo$Operation$1.class~EnumTypeGuiDemo$Operation$4.classEnumTypeGuiDemo$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.
为什么枚举类中可以定义抽象方法
把上面三个样例汇总,规则可以拆成三条:
- 抽象方法的例外 (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.
- 常量类体就是匿名子类 (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.
- 必须全部有实现 (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;
...
- 声明了抽象方法的枚举类被标记为
abstract(OrderStatus没有抽象方法,就没有这个修饰符,Modifier.isAbstract分别返回true/false) - 每个带类体的常量,
new的是对应的匿名子类;("PLUS", 0)里的0就是 ordinal - 所有实例放进
$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);
}
}
}
}