跪拜 Guibai
← Back to the summary

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:

  1. eval is an abstract method in the enum; where is its implementation?
  2. You cannot new an 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:

image.png

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.

image.png

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

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:

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.

image.png

GUI Overview

The window has three panes:

The three samples cover the three cases:

Sample 1: No Constant Has a Class Body

Direction is the most ordinary style:

enum Direction {
    EAST, WEST, SOUTH, NORTH
}

image.png

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):

image.png

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:

image.png

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):

image.png

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:

image.png

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:

image.png

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 sealed if 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.

image.png

Simplified class diagram for OrderStatus$1 ~ OrderStatus$3 (omitting generics):

image.png

Why Can an Abstract Method Be Defined in an Enum?

Summarizing the three samples, the rule splits into three parts:

  1. 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 abstract method m must appear directly within an abstract class (call it A) unless it occurs within an enum declaration (§8.9); otherwise, a compile-time error occurs.

    image.png

  2. 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).

    image.png

  3. All constants must provide implementations (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.

    image.png

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;
  1. The enum class declaring an abstract method is marked abstract (OrderStatus has no abstract method, so it lacks that modifier; Modifier.isAbstract returns true / false respectively).
  2. For each constant with a class body, new instantiates the corresponding anonymous subclass; the 0 in ("PLUS", 0) is the ordinal.
  3. All instances go into $VALUES, and values() 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:

image.png

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;
    }
}