Java Enums: Why Abstract Methods Work, and the Anonymous Subclass Behind Every Constant
Introduction: An Enum That Seems Not to Make Sense
Let's start with a piece of code that compiles and runs normally:
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());
}
}
Save this as Operation.java. Compile and run with these commands:
javac Operation.java
java Operation
Output:
3.0
Operation$1
Operation.PLUS.eval(1, 2) returns 3.0. But two things seem off:
evalis an abstract method in the enum; where is its implementation?- You cannot
newan enum, nor can you easily extend it like a normal abstract class, so where is the implementation class?
The JDK leaves a clue. The comment on Enum.getDeclaringClass() (I referenced Enum.java in JDK 21) says:
The method's code is only a few lines:
@SuppressWarnings("unchecked")
public final Class<E> getDeclaringClass() {
Class<?> clazz = getClass();
Class<?> zuper = clazz.getSuperclass();
return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
}
In other words: some constants have a runtime class that is not the enum class itself, but a subclass of the enum class. JLS §8.9.1 says:
The optional class body of an enum constant implicitly defines an anonymous class declaration (§15.9.5) that extends the immediately enclosing enum type.
The { ... } after each constant in Operation.java indicates that each constant is an anonymous subclass, and the abstract method's implementation is written inside. This post uses a small GUI tool to put the three layers — "enum class → constant → constant's runtime class" — together, and compares the results of getClass() and getDeclaringClass() one by one.
Notes
- The full code uses
recordand text blocks, requiring JDK 16+ (at the time of writing, I used JDK 21) - Only public APIs are used; no
--add-opensneeded: the GUI usesgetClass(),getDeclaringClass(),isAnonymousClass(), etc.; reflection supplementary evidence usesisSealed(),getPermittedSubclasses(),getDeclaredMethods()— none touch private members - Names like
Operation$1are an implementation detail ofjavac. The$1,$2shown in this post and GUI are just the numbering given by my localjavac; other compilers may number them differently — when reading, focus on "these subclasses exist" rather than the exact numbers
Main Text
I used trae to generate GUI code that can compare "enum source / constant / runtime class". For coherence, I put the full code in the Full Code section.
How to Run
Save the Full Code as EnumTypeGuiDemo.java. Compile it:
javac EnumTypeGuiDemo.java
After compilation, you get these class files:
EnumTypeGuiDemo.class,EnumTypeGuiDemo$1.class(anonymousDefaultTableModelin the GUI, unrelated to enums),EnumTypeGuiDemo$Sample.class,EnumTypeGuiDemo$FactRenderer.class,EnumTypeGuiDemo$HierarchyPanel.classDirection.classOperation.class,Operation$1.class~Operation$4.classOrderStatus.class,OrderStatus$1.class~OrderStatus$3.class
Operation$1 ~ Operation$4 and OrderStatus$1 ~ OrderStatus$3 are the stars: anonymous subclasses generated from constant class bodies.
Run the main method with:
java EnumTypeGuiDemo
After starting, the window title is enum constant type explorer.
GUI Overview
The window has three panes:
- Left pane
sample (enum type): three enum samples;constants (values()): listed invalues()order, i.e. declaration order - Middle pane
runtime class hierarchy (computed from getSuperclass()) - Right pane: a summary line, 9 rows of
check / result, andenum source— the source of the current sample, with the selected constant bolded and light-blue highlighted, making it easy to see if there is a class body in the source
The three samples cover the three cases:
- No constant has a class body
- All constants have class bodies
- Some constants have class bodies
Sample 1: No Constant Has a Class Body
Direction is the most ordinary style:
enum Direction {
EAST, WEST, SOUTH, NORTH
}
Select EAST: on the right, getClass().getName() and getDeclaringClass().getName() both return Direction, and getClass() == E.class and getDeclaringClass() == E.class are both ✔ true — getClass() and getDeclaringClass() are exactly the same. The middle pane shows only one box, with a green line below:
every constant is a plain instance of Direction - no subclasses were generated
A simplified class diagram for Direction (omitting generics):
Sample 2: Every Constant Has a Class Body
All four constants of Operation have class bodies, each implementing the eval method. The screenshot shows TIMES selected:
Summary line: 4 constants, 4 with class bodies, 5 class files (1 enum class + 4 anonymous subclasses).
Right pane nine rows (using TIMES as an example):
| check | result |
|---|---|
name() |
TIMES |
getClass().getName() |
Operation$3 |
getClass().getSimpleName() |
(empty string) |
getClass().getCanonicalName() |
null (anonymous class) |
getClass().isAnonymousClass() |
true |
getDeclaringClass().getName() |
Operation |
has class body |
yes |
getClass() == E.class |
✘ false |
getDeclaringClass() == E.class |
✔ true |
getClass() returns Operation$3, but getDeclaringClass() returns Operation. This matches the Enum.getDeclaringClass() implementation: if the superclass is not java.lang.Enum, take the superclass.
Running javap -p Operation\$3 shows the structure (-p shows even the private constructor):
Compiled from "EnumTypeGuiDemo.java"
final class Operation$3 extends Operation {
private Operation$3(java.lang.String, int);
double eval(double, double);
}
The enum class Operation itself is compiled as an abstract class (from javap -p Operation):
Compiled from "EnumTypeGuiDemo.java"
abstract class Operation extends java.lang.Enum<Operation> {
public static final Operation PLUS;
public static final Operation MINUS;
public static final Operation TIMES;
public static final Operation DIVIDED_BY;
private static final Operation[] $VALUES;
public static Operation[] values();
public static Operation valueOf(java.lang.String);
private Operation();
abstract double eval(double, double);
private static Operation[] $values();
static {};
}
The abstract modifier can also be verified by reflection: Modifier.isAbstract(Operation.class.getModifiers()) returns true.
If any constant misses the implementation, compilation fails. For example, change MINUS to have no class body:
enum Operation {
PLUS { double eval(double x, double y) { return x + y; } },
MINUS;
abstract double eval(double x, double y);
}
javac reports the error pointing at MINUS (JDK 21, Chinese locale):
Operation.java:3: 错误: Operation是抽象的; 无法实例化
MINUS;
^
1 个错误
Simplified class diagram for Operation$1 ~ Operation$4 (omitting generics):
Sample 3: Only Some Constants Have Class Bodies
OrderStatus resembles a real business pattern: the isTerminal() method lets subclasses override its behavior, and one constant keeps an empty class body:
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; }
}
The GUI shows all three kinds of constants in one diagram:
- The root node
OrderStatusbox listsCREATED, PAID, SHIPPED; theirgetClass()is the enum class itself. ON_HOLD,COMPLETED,CANCELLEDeach occupy an anonymous subclass box:OrderStatus$1OrderStatus$2OrderStatus$3
Select ON_HOLD: the right pane's has class body is yes, and getClass() == E.class is ✘ false. Although this class body is empty, javap -p OrderStatus\$1 shows it only has a constructor:
Compiled from "EnumTypeGuiDemo.java"
final class OrderStatus$1 extends OrderStatus {
private OrderStatus$1(java.lang.String, int);
}
In other words, an empty class body writes no methods, but it still has its own class. So getClass() does not equal the enum class.
Select the terminal COMPLETED: the highlight shows OrderStatus$2:
Notably, getDeclaringClass() returns OrderStatus in all six cases; the last row of the check table is always ✔ true.
One more point: as soon as at least one constant has a class body, the enum class becomes sealed. OrderStatus.class.isSealed() returns true, and getPermittedSubclasses() lists exactly the three anonymous subclasses:
[class OrderStatus$1, class OrderStatus$2, class OrderStatus$3]
JLS §8.9 states:
An enum class E is implicitly
sealedif its declaration contains at least one enum constant that has a class body. The permitted direct subclasses (§8.1.6) of E are the anonymous classes implicitly declared by the enum constants that have a class body.
Simplified class diagram for OrderStatus$1 ~ OrderStatus$3 (omitting generics):
Why Can an Abstract Method Be Defined in an Enum?
Summarizing the three samples, the rule splits into three parts:
Exception for abstract methods (JLS §8.4.3): In ordinary classes, an abstract method must be declared in an abstract class; JLS carves out a separate exception for enum declarations:
The declaration of an
abstractmethodmmust appear directly within anabstractclass (call it A) unless it occurs within an enum declaration (§8.9); otherwise, a compile-time error occurs.Constant class body is an anonymous subclass (JLS §8.9.1):
The optional class body of an enum constant implicitly declares an anonymous class (§15.9.5) that (i) is a direct subclass of the immediately enclosing enum class (§8.1.4), and (ii) is
final(§8.1.1.2).All constants must provide implementations (JLS §8.9.2):
It is a compile-time error if an enum declaration E has an
abstractmethodmas a member, unless E has at least one enum constant and all of E's enum constants have class bodies that provide concrete implementations ofm.
Combined: abstract methods are implemented by the "anonymous subclasses of constants"; the enum class itself need not implement them, so the compiler permits this set of declarations.
The compiler does three specific things; you can see them in javap -c's static {} (using PLUS as an example; the other three constants work similarly):
static {};
Code:
0: new #35 // class Operation$1
3: dup
4: ldc #37 // String PLUS
6: iconst_0
7: invokespecial #38 // Method Operation$1."<init>":(Ljava/lang/String;I)V
10: putstatic #3 // Field PLUS:LOperation;
...
52: invokestatic #51 // Method $values:()[LOperation;
55: putstatic #16 // Field $VALUES:[LOperation;
- The enum class declaring an abstract method is marked
abstract(OrderStatushas no abstract method, so it lacks that modifier;Modifier.isAbstractreturnstrue/falserespectively). - For each constant with a class body,
newinstantiates the corresponding anonymous subclass; the0in("PLUS", 0)is the ordinal. - All instances go into
$VALUES, andvalues()returns them externally.
So the real receiver of Operation.PLUS.eval(1, 2) is Operation$1, not Operation. The static field PLUS still has type Operation; polymorphism operates normally here.
Consequences of This Rule
1. getClass() compares runtime type, getDeclaringClass() compares enum type
Operation.PLUS.getClass() == Operation.MINUS.getClass(); // false
Operation.PLUS.getDeclaringClass() == Operation.class; // true
JDK's own Enum.compareTo is written that way:
It first uses getClass() for a fast check (many enums have no class bodies, so they are already equal), and only falls back to getDeclaringClass() when they differ.
2. Anonymous classes have empty metadata
getSimpleName() returns an empty string, getCanonicalName() returns null, isAnonymousClass() returns true. More thoroughly: OrderStatus$1.isEnum() is false, and getEnumConstants() is null — the runtime class of this constant isn't even considered an enum class. You can use getDeclaringClass() to determine which enum it belongs to.
Full Code
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
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.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.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
*/
public class EnumTypeGuiDemo {
/** 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) {
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();
}
/** Operation$1 -> Operation$1, Direction -> Direction (default package). */
private static String shortName(Class<?> type) {
String name = type.getName();
int lastDot = name.lastIndexOf('.');
return lastDot >= 0 ? name.substring(lastDot + 1) : name;
}
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;
}
}
}
/** 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;
}
}