Java's enum looks like a language feature with a type system behind it. In the class file it is a flag bit in two different flag tables and a list of static fields — there is no enum structure anywhere in the format, no enum attribute, and nothing that records "these fields are the constants".
Which makes an enum the format's clearest case of a type with a compile-time contract expressed through conventions the machine does not enforce. The JVM knows an object is an enum because its superclass is java.lang.Enum, and java.lang.Enum implements the values() and valueOf() machinery by reflecting over the declared fields. The format stores the fields. The meaning of the fields is entirely in the class hierarchy and in java.lang.Enum's own code.
So an enum is the opposite of a record, and worth reading beside it. A record's declaration is a first-class attribute. An enum's is a flag bit plus a convention. One shape is machine-readable and enforced; the other is reconstructable at run time by reflection over a superclass in the JDK.
There is no enum structure to model, which is itself the model. The whole encoding is four facts:
Fact 3 is the one that catches people, and it is the third appearance of the multiple-flag-tables lesson from Module 2. 0x4000 is ACC_ENUM in both the class table and the field table, and it means the same thing in each: "this is an enum" and "this field is an enum constant". The bit is shared because the concept is shared, and that is a genuine convenience — unlike InnerClasses's 0x0008, which means different things in different tables. A reader needs the right table for the right field, but for this bit the answer happens to agree.
And fact 2 is what makes the format's silence workable. java.lang.Enum contains a private static array that every enum subclass shares, keyed by the subclass, and it is initialised by scanning the subclass's declared fields for ACC_ENUM. That is the whole discovery mechanism: a reflection pass over fields, filtering on the flag bit.
A two-constant enum nested inside another class, and every member with its flags. This is the complete Inner$E.class:
flags: (0x4030) ACC_FINAL, ACC_SUPER, ACC_ENUM
public static final Inner$E A; flags: 0x4019
PUBLIC | STATIC | FINAL | ENUM
public static final Inner$E B; flags: 0x4019
PUBLIC | STATIC | FINAL | ENUM
private static final Inner$E[] $VALUES; flags: 0x101a
PRIVATE | STATIC | FINAL | SYNTHETIC
public static Inner$E[] values(); flags: 0x0009
PUBLIC | STATIC <- NOT synthetic
public static Inner$E valueOf(String); flags: 0x0009
PUBLIC | STATIC <- NOT synthetic
private Inner$E(); flags: 0x0002
PRIVATE
private static Inner$E[] $values(); flags: 0x100a
PRIVATE | STATIC | SYNTHETIC
static initialiser flags: 0x0008
STATIC
Six source words — enum E declaring constants A and B — produce eight members, and the flags are more informative than the names.
values() and valueOf(String) are 0x0009: ACC_PUBLIC | ACC_STATIC and nothing else. No ACC_SYNTHETIC bit. The natural expectation — that compiler-generated members are marked synthetic, and the Code concept's discussion of bridge and synthetic methods — says otherwise.
The reason is that they are not generated implementations at all. They are real, small methods whose bodies call java.lang.Enum's protected machinery, and they are part of the enum's public contract: Colour.values() in source compiles to invokestatic values, and the compiler must be able to resolve that name. A method that source code calls by name cannot be synthetic, because synthetic means "not part of the language's surface" and these are about as much surface as a class has.
Which makes the contrast with the other two entries the point of the concept. $VALUES and $values() are genuinely synthetic — the former is the cached array, the latter is a bridge that exists so java.lang.Enum's own internals can reach it. Neither is callable from source, and both carry 0x1000. So the file contains two kinds of compiler-added member and the flag distinguishes them correctly: the ones the language exposes are unmarked, the ones the implementation needs are marked. A tool that hides synthetic members will correctly hide the array and show the API.
ACC_FINAL on the class (0x4030 includes 0x0010) and ACC_PRIVATE on the constructor are the same constraint stated twice, and they are what stop the two things an enum must not be: subclassed, and instantiated from outside.
The superclass is the other half. Inner$E extends java.lang.Enum<Inner$E> — java.lang.Enum's constructor is protected Enum(String name, int ordinal), and the enum's private constructor calls it with the name and the ordinal. So the constant's name and position are passed to a superclass that stores them, and toString() returns the name and ordinal() returns the position, both inherited unchanged. The enum's identity is established once, in the superclass constructor, and the class file only has to supply the two values.
Which explains the one piece of machinery the format does provide: java.lang.Enum's $VALUES array, per subclass, initialised by the static initialiser. The <clinit> shown above is the method that creates each constant in declaration order, assigns $VALUES, and is why an enum cannot be initialised cyclically — the order is the source order, fixed at compile time.
A has descriptor LInner$E; — the enum's own type, not Object and not java.lang.Enum. So each constant is a field whose type is the class that declares it, and the static initialiser constructs an instance of the class being initialised. A class whose static initialiser constructs instances of itself is legal only because the field is not final-read at verification time in a way that would fail — and the verifier accepts it because the constants' ACC_FINAL is satisfied by the initialiser rather than by a compile-time constant, which is a distinction that exists specifically to make this pattern work. The two flags on the constant, ACC_ENUM and ACC_FINAL, therefore mean different things at the bytecode level: one says "this is a constant", the other says "this is a constant of an enum".What an enum costs and what it buys, told as the mechanism a tool would have to write. The class file records the fields; the semantics live in java.lang.Enum and are recovered at run time by a reflective scan:
That is why there is no enum attribute. The constant list is already in the file, in a form reflection can enumerate, tagged with a bit that says which of the fields count. An explicit list of constants would duplicate information the file already carries in a machine-parseable way, and the escape hatch's whole design philosophy says do not add a structure for something already present.
So the trade is this. The format stores less and the machine recovers the rest, which is why an enum is a few hundred bytes of fields and a <clinit> rather than a table of names and ordinals. And the cost is that the guarantee is only as strong as the convention: a class with ACC_ENUM that does not extend java.lang.Enum is not an enum, and a class that does extend it with ACC_ENUM fields is an enum whether or not the source said enum. The flag is a hint to the reflective scan, not a checked declaration — the opposite of a sealed class's permitted list, which the verifier enforces.
And the ordinal deserves a note, because it is the thing people get wrong. The ordinal is the field's declaration index among the ACC_ENUM fields, and it is not stable. Reordering the constants changes every ordinal after the moved one, and serialising an ordinal and deserialising it against a reordered enum gives a silently wrong constant — not an error, a wrong answer. That is a real hazard and it is a direct consequence of the encoding: the ordinal is not recorded anywhere in the file, it is derived from field order, and nothing in the class file ties it to a source declaration. A format that stored the ordinal explicitly could detect the mismatch; one that derives it cannot.
Compare that to how the same problem is handled elsewhere in this collection. A COFF relocation's ordinal is a position in a symbol table, so a reordered table invalidates it — and the tool that reads it knows the ordinal is a position. A PE import ordinal is likewise a position, and the loader resolves it against the import directory. In both, an ordinal is a resolved position; in Java's enums, the ordinal is a derived one that nothing checks. That is the same category of hazard as a table index that can go stale, and it is the clearest reason to prefer names when persisting anything derived from a field order.
$ javap -v -p out/Inner\$E.class
$ python3 courses/jvm/assets/samples/crosscheck.py classes/Inner\$E.class$VALUES, values, valueOf, the constructor, and $values. Then sort them into the ones the source implied and the ones it did not — four of the eight exist in no source file.values() and valueOf(String) are 0x0009 with no synthetic bit, while $VALUES and $values() are 0x101a and 0x100a. Explain why the two public methods are not synthetic, and the answer — source code calls them by name — is the whole reason.ACC_ENUM, printing what java.lang.Enum does. That is the format's entire enum semantics, executed in front of you — and it explains why there is no enum attribute.ordinal(). Every constant after the swap has a different ordinal — and nothing in either class file records the old one, because the ordinal was never stored.ACC_ENUM on a non-enum static field. A class extending java.lang.Enum with an extra static final field carrying the bit. It shows up in values() — the flag is a selector for a reflective scan, not a checked declaration, and that asymmetry with sealing is the concept's real lesson.name and one descriptor Utf8, two CONSTANT_Strings, plus the <clinit> body — and the Enum.name/ordinal strings are shared across every enum in the file. A little arithmetic on where the size goes, and it is not where a reader expects.Without looking back: an enum's two constants are public static final fields with flags 0x4019, the class has flags 0x4030, and values() has flags 0x0009. Which of those three flag values tells the machine something it could not work out otherwise, and what specifically happens if values() carried ACC_SYNTHETIC? Would the class still load?
You are writing a decompiler. It reads the class flag table, finds ACC_ENUM, and reports the class as an enum with the constants listed from the fields carrying the bit. On one class it reports four constants when the source declared two. The class extends java.lang.Enum correctly and the file is otherwise valid. What has happened, and what is the general principle about which kinds of format fact can be trusted without a check?
The general habit: a fact the format checks is trustworthy on its own; a fact it merely stores is trustworthy only under a convention some other component owns. Find out which you are reading before you rely on it.
"Derived from a field order and therefore unstable" is a hazard this collection has covered three times, and an enum's ordinal is the worst version of it because nothing fails loudly. Compare a PE import by ordinal and a COFF relocation's symbol ordinal: both are positions, both are resolved by a consumer against a table the producer wrote, and both are checked at use — an out-of-range ordinal is a load-time or link-time failure. An enum's ordinal is a position that no consumer resolves, because the consumer computed it once at class initialisation from the field order it read. The same shape, and none of the safety, which is why the Java rule is "never persist an ordinal" rather than "an out-of-range ordinal will be caught".
The reflective recovery is a mechanism the format leans on repeatedly, and it is the counterpart to what a WebAssembly custom section is for: a place to put information the core format does not model. There the difference is that custom sections are inert data for a tool to read, while here the JDK interprets the flags to build the enum's behaviour. One is a container the format knows nothing about; the other is a bit the format defines and a library gives meaning to. The consequence is that an enum's semantics are upgradeable — a newer java.lang.Enum could add methods without a class file change — and that is a real architectural choice rather than an accident.
The two-way nature of the flag tables is a good closing note for this module. 0x4000 means the same thing in the class table and the field table, and 0x0008 means different things in the class table and the member table, and InnerClasses uses the member table for what looks like a class. Three concepts, three flag tables, and a bit that is ambiguous in one position and not in another — which is the Module 2 lesson arriving repeatedly, and the strongest general habit in this course: when a format has more than one kind of record, it has more than one vocabulary, and the values are identical while the meanings are not.
What the three shapes in this module share is more interesting than how they differ. A record's components are a declared list. A sealed class's permitted subclasses are a checked list. An enum's constants are a derived list. Three answers to the same question — how does the machine learn what a type contains — on one spectrum from most explicit to least: an attribute the compiler wrote, a list the verifier enforces, a flag a library interprets. And the module is the argument that this spectrum is a real design axis, because all three shapes shipped in the same era and each reached for the mechanism its semantics actually required.
Module 5 moves from what a type is to how it is connected: the invokedynamic instruction and its bootstrap methods, which is where the class file stopped being a description of code and became a program for producing code at load time.