Every other class shape in this course so far has been about describing something. A record describes its components. An enum describes its constants. Sealing does something else: it constrains what may exist. The permitted list is not information about the class; it is a rule about the class's future and about everyone else's.
That makes it the only attribute in this module that the JVM enforces. A wrong record attribute is inert. A wrong signature is unchecked. A wrong PermittedSubclasses list is a load-time rejection, because the verifier has to check that a class claiming to extend a sealed type is actually on the list — and it can only do that by reading the list.
So this is a small attribute with a large job, and the interesting parts are what it deliberately does not say. It does not say the subclasses exist. It says who is allowed to be one, and the permitted subclass's own file is what makes it one.
The whole attribute, in its entirety:
This is the shortest meaningful attribute in the format. No flags, no count of flags, no per-entry structure, no version. Compare Record's six bytes per component, or InnerClasses's eight. A reader for this attribute is a two-line loop.
And the shape is worth contrasting with the two attributes it most resembles, because they are byte-identical in layout and mean different things. From Sealed.class:
PermittedSubclasses: 00 02 00 0a 00 08
| | |
| | +-- #8 = Sealed$B
| +-------- #10 = Sealed$A
+-------------- TWO permitted subclasses
NestMembers: 00 06 00 1b 00 1d 00 1f
| 00 21 00 12 00 07
+-- also a count and a list of u2
Identical structure, different meaning, and no way to tell them apart from the bytes — which is the escape hatch's property again, and a good one to internalise: the attribute name carries all the semantics and the body carries none. A reader cannot infer that one is about inheritance and the other about access; it has to know which name it is holding.
What a sealed hierarchy actually looks like in a class file, and the detail that surprises: the attribute appears on every class in the hierarchy, not just the parent.
Sealed.class (the parent)
PermittedSubclasses -> Sealed$A, Sealed$B
InnerClasses:
public static final Sealed$B of class Sealed
... one record per permitted subclass, giving each the
ACC_FINAL | ACC_PUBLIC that sealing implies
Sealed$A.class
PermittedSubclasses -> EMPTY (0 bytes of body)
An empty PermittedSubclasses is the marker for "sealed and permits nothing further". That is a real and slightly odd design: the attribute's presence is what says "sealed", and the list can be zero-length. A sealed class permitting no subclasses is legal and means the hierarchy is closed at exactly itself — and the way to express that is an attribute with an empty body, which is four bytes of header and nothing else.
This is the opposite of the record situation, and the contrast is instructive. For a record, the attribute's content is the declaration. For a sealed class, the attribute's presence is the declaration and its content is a permission list — so an empty content is meaningful rather than degenerate, and a reader that checks "is the list non-empty" to decide sealedness gets it exactly backwards.
Every permitted subclass in the sample is public static final in the InnerClasses record. The ACC_FINAL is not a coincidence and not optional — a permitted subclass must be final, because a class that could be extended could be extended by something not on the list, which defeats the entire mechanism.
That is the one constraint sealing adds to the subclass, and the format enforces it: the verifier rejects a permitted subclass that is not final. So the two attributes are coupled — PermittedSubclasses on the parent permits, and the ACC_FINAL on the child's InnerClasses record seals the permission from propagating. A tool that reads the permitted list and forgets to check the child's flags will report a hierarchy that is not actually closed.
But a permitted subclass does not have to be a class. An interface may be permitted, and an interface cannot be final — so the final rule does not apply there. That is the asymmetry in the rule, and it is derived from the language rather than from the encoding: sealing an interface restricts who may implement it, and implementations are not subtypes in a way that can be further sealed.
superclass. This is the same two-sided arrangement as the nest, and for the same reason: a one-sided claim is just an assertion. Here, though, the failure mode is a compile error rather than a load error, because javac checks both directions and reports "cannot extend sealed class" or "sealed class missing permitted subclass" as source errors. What the JVM adds is the load-time check, which catches a hand-edited or maliciously edited file that javac would never produce — and that check is the reason this attribute is the enforced one.The language restricts who may be a permitted subclass, and those restrictions are a source rule rather than a format one, but they explain why the file sometimes looks sparse. Two rules at the source level: permitted subclasses must be in the same package as the sealed class, or in the same module if the sealed class is in one. The second rule is why a sealed class in a named module can permit a class from a different package, and why one in the unnamed classpath cannot.
Nothing in the six bytes records which rule applied. The attribute is just a list of names, and the package relationship is a fact about the names — which a verifier can only check by loading the referenced classes. So a permitted subclass in a class file that the verifier cannot resolve is a problem at load time, and that is one reason hand-building these hierarchies is fiddly: the parent's file names classes that must exist elsewhere.
What the enforcement is actually for. Sealing exists to let the compiler and the machine reason exhaustively about a type hierarchy — and the payoff is a switch that is checked at compile time:
That is the whole reason for the attribute. Exhaustive switching is a compile-time proof that needs the set of subtypes to be known and finite, and a list of permitted subclasses in a class file is exactly that set. Without the attribute, the compiler would have to know the hierarchy from the source of every class involved, and a class file would carry nothing that a downstream tool could use — and record deconstruction, the feature in the previous concept, would be impossible, because destructuring needs the component list for the same reason exhaustive switching needs the subtype list.
So both of the newest features rest on the same principle, and it is the principle this module has been building: the format records what the language's reasoning needs, as a first-class structure, rather than leaving the machine to infer it. A record's component list and a sealed class's permitted list are the same kind of fact — an ordered or unordered set of names the compiler must consult — and they are both things a class file could plausibly have omitted on the grounds that the compiler knows them already.
And the honest limit, which is where the sealed design is genuinely more constrained than the record one. Sealing is enforced by the verifier, so a wrong list is a load-time rejection; records are enforced by nothing, so a wrong list is inert. That asymmetry is a consequence of what each feature is for. Exhaustiveness is a correctness property — a missed case is a wrong answer at run time — so the machine checks it. A record's components are a description, and a description the machine does not need cannot be worth a check.
Compare that to how a comparable constraint is handled elsewhere in this collection. A PE's GuardCFCheckFunctionPointer is a claimed function that the loader verifies before using — enforcement, not description. A COFF weak external is a claim the linker resolves rather than trusts — also enforcement. A WebAssembly module's declared imports must be provided or instantiation fails — enforcement again. Whenever a format has a list that constrains what can exist rather than describing what does, it usually checks it, because the list's whole purpose is the constraint.
$ javap -v -p out/Sealed.class
$ python3 courses/jvm/assets/samples/crosscheck.py classes/Sealed.classu2 count and a loop. Then write Record's reader and see how much longer it is — two attributes that a scan reports as similar and a parser treats as nothing alike.Sealed$A.class's attributes. There is a PermittedSubclasses with a body of zero bytes, and its presence is the only thing saying "sealed, permits nothing". A reader testing for a non-empty list gets this exactly backwards.ACC_FINAL. Take a permitted subclass's InnerClasses record and clear its final bit, fixing lengths. The verifier will reject it — and the rejection is the interesting part, because the permitted list still says the same thing. The two attributes are coupled.Record attribute you corrupt, which loads fine.sealed interface S permits Impl with an empty body, and dump both files. The permitted entry is a class index to an interface and carries no final, because an interface cannot be final — and working out what stops the interface's implementations from propagating is the interesting part.PermittedSubclasses and NestMembers are the same shape. Take a reader that dispatches on layout instead of on name and pass it a file containing both — it will produce confident, plausible, wrong output, which is the sharpest possible demonstration that the name carries all the meaning.Without looking back: a class file has a PermittedSubclasses attribute whose attribute_length is 0 — present, but an empty body. One of its permitted subclasses has a class file whose InnerClasses record gives it ACC_PUBLIC | ACC_SUPER with no ACC_FINAL. The JVM loads the parent. What is true of the hierarchy, and what happens if a tool tries to use it for exhaustive switching?
You are writing a code generator that emits a sealed interface and its implementations. It writes the permitted list, and it marks each implementation final. The generated code compiles and the classes load. Then a user writes a subclass of one of the implementations, and the compiler accepts it — the hierarchy is not actually closed, and the generator's exhaustiveness analysis is wrong. What did the generator have to do differently, and why is the mistake invisible in the files it produced?
The general habit: when a format expresses a rule by pairing something a producer records with something a consumer enforces, the producer's half is never evidence that the rule holds. Check the consumer's half, because that is the half that enforces it.
A list of names that constrains what may exist is one of the oldest ideas in linking, and the format list gives the same guarantee by a different mechanism. An ELF symbol's binding restricts which kind of reference may name it, and a COFF weak external restricts what happens when the symbol is absent — in all three cases a table of names in one file constrains what another file may do, and the constraint is checked rather than trusted. Compare a bare .string in an ELF string table, which names things but constrains nothing.
The difference is who does the checking, and it is the same difference as in the previous concept. A sealed class's list is checked by the JVM when it resolves the hierarchy; a COFF weak external's is checked by the linker — and in both cases the check happens in a different file from the one that declares. That is why the ACC_FINAL bug above is invisible to a per-file reader, and it is a general property of any cross-file constraint: the declaration and the enforcement are in different places, so a tool that checks in one place is checking half of it.
Exhaustiveness as a compile-time proof has an exact counterpart in WebAssembly's structured control flow, where the requirement that a function's block type and result type line up is checked by validation rather than trusted from the producer. And it has a counterpart in a PE's guard flags, where the security policy has to be verifiable from the file rather than from the build system's intentions — in each case the machine verifies a property because the producer is not necessarily trustworthy.
The empty-list case is the part of this attribute that has no clean counterpart elsewhere, and it is worth sitting with. Four bytes of header and zero bytes of body is a complete, meaningful, loadable statement that this type is closed over itself. Compare an absent attribute, which means "not sealed". The three states — absent, present-and-empty, present-and-populated — are all distinct and all meaningful, and only the middle one is a case where the content is not the data. An ELF section header with a zero size is a similar shape: the structure is there and it declares nothing, and both mean something.
Finally, the "sealed and permits nothing" pattern is the format's clearest case of the presence of an attribute carrying information its content does not, and that sits awkwardly beside the escape hatch's usual bargain. The mechanism was designed so a reader could skip a name it did not recognise, on the grounds that skipping is always safe. For a record, skipping loses the component list, which only matters to someone doing reflection. For a sealed class, skipping loses a constraint the verifier is supposed to enforce — so this is an attribute a reader cannot safely skip if it is verifying anything, and that is a real qualification to the "skip unknown attributes" rule that is worth naming rather than glossing.