Java's nested classes are not a language feature with a file format representation. They are a naming convention, two attributes, and a great deal of compiler bookkeeping — and the gap between what the source says and what the file contains is where all the difficulty is.
The source says static class Nested and gets a file called Inner$Nested.class. Nothing in the source names that file. Nothing in the format requires it to be called that. And for the classes the compiler invents — anonymous ones, local ones declared inside methods — there is no name in the source at all, yet a file appears.
So the compiler has to invent names, record what it invented, and record the relationships the language needs at run time. That is three attributes' worth of work, and each one answers a different question. InnerClasses records what the compiler decided. NestHost and NestMembers record what the language permits. Getting that distinction straight is the difference between reading these attributes and merely decoding them.
Three attributes, and the shape of each is different — which the previous concept would have predicted:
And the three answer three different questions, which is the whole design:
InnerClasses is descriptive and duplicated. It appears on the outer class and on the inner one, and both copies say the same thing. It exists so a reader holding only the nested class's file can still discover the nesting, without loading the outer class.NestHost is one index, and it is the modern replacement for the synthetic accessor problem. A nested class needs to reach its outer class's private fields. Before Java 11 the compiler generated synthetic accessor methods; from Java 11 the nest is declared instead, and every member of the nest may read every other member's private state, checked by the verifier rather than by generated methods.NestMembers is the outer class's list of who is in the nest, and it is what lets the verifier check a private access without loading the other class. It is the index form of the same fact.That last distinction has a concrete consequence worth holding onto. InnerClasses is about names; the nest is about access. They are populated independently — verified below — and a class can be in a nest without appearing in any InnerClasses record, and vice versa. A tool that assumes the two lists are the same will be wrong on real input.
One source declaration, six files, and every InnerClasses record decoded from the raw bytes of Inner.class:
0053: 00 06
| +-- SIX records
+----- SIX nested classes, 8 bytes each
[0] inner=#7 outer=#0 name=#0 flags=0x0000
class = Inner$1 <- the ANONYMOUS Runnable
outer = (none) name = (none)
[1] inner=#18 outer=#0 name=#36 flags=0x0000
class = Inner$1Local <- declared INSIDE A METHOD
outer = (none) name = "Local"
[2] inner=#27 outer=#13 name=#37 flags=0x4018
class = Inner$E enum E
flags = ACC_FINAL ACC_ENUM 0x0008
[3] inner=#29 outer=#13 name=#38 flags=0x0608
class = Inner$Iface interface Iface
flags = ACC_INTERFACE ACC_ABSTRACT 0x0008
[4] inner=#31 outer=#13 name=#39 flags=0x0000
class = Inner$Member class Member, a non-static inner class
flags = (none)
[5] inner=#33 outer=#13 name=#40 flags=0x0008
class = Inner$Nested static class Nested
flags = 0x0008
Five things in that table are each a finding, and two of them contradict what a reader would guess.
Records [2], [3] and [5] all carry 0x0008, and 0x0008 is ACC_STATIC. A class cannot be static — top-level classes have no static, and ACC_STATIC in the class flag table means nothing at all.
But these are not classes in the top-level sense. inner_class_access_flags describes a member of a class, so it uses the field/method flag table, not the class one. And a nested enum and a nested interface are both implicitly static, which is a language rule: a nested interface is static because an interface cannot hold instance state, and a nested enum is static because an enum's instances are supposed to be singletons. Record [5] is the explicit case — the source said static class Nested and the bit records exactly that.
ACC_FINAL ACC_ENUM UNKNOWN(0x0008) — which is exactly right and also exactly the bug the Module 2 concept warned about. The decoder used the class flag table because it was asked to render class flags, and 0x0008 is not in that table, so it honestly reported an unknown bit. The bits are identical to the member table; only the table that knows their names is different. A reader that renders inner_class_access_flags with the class table will show UNKNOWN for every nested interface and every nested enum in the file, and a reader that renders them with a merged table will show ACC_STATIC for a top-level class that is not static. Neither is catastrophic and both are wrong, which is the worst shape for a bug.Record [0] is the anonymous Runnable, and it is the reason the file is called Inner$1.class. The compiler had to invent a name and recorded nothing:outer_class_info_index = 0 and inner_name_index = 0. A class with no name and no recorded outer class is the file's entire justification for the $1 convention — the number is the only identity an anonymous class has, and it is a counter of anonymous classes within the outer class, starting at 1.
Record [1] is the class declared inside a method, and it is the interesting one: it has a name ("Local") but no outer class recorded. The source was void use() that declares class Local, then news one, so Local is scoped to one method and the compiler emitted Inner$1Local.class — a $1and a name, which is not what either convention predicts on its own.
The numbering scheme behind that name is worth stating because it is entirely implementation-defined and the format does not care:
Inner$1 the first ANONYMOUS class in Inner
Inner$1Local a LOCAL class: $1 because the anonymous Runnable was
compiled first, then the simple name
Inner$Nested a named nested class: just Outer$SimpleName
Inner$E a nested enum: Outer$SimpleName
Inner$Iface a nested interface: Outer$SimpleName
Inner$Member a non-static inner class: Outer$SimpleName
the rule javac actually uses: an ANONYMOUS class gets Outer$N where N
counts anonymous classes; a LOCAL class gets Outer$NLocal where N is
the counter at the time it was compiled; a NAMED nested class gets
Outer$SimpleName. All three are conventions, not rules -- nothing in
the format requires any of them, and every JVM accepts any name.
Which brings up the question the descriptor grammar only hinted at: the $ in LDesc$Nested; is not a nesting marker. It is a legal character in a Utf8 entry, the compiler happens to use it to join an outer and inner name, and a class genuinely named A$B in Java is indistinguishable in the file from a nested class B of A. The method reference in Module 1 makes it concrete — this::id compiles to a synthetic method named lambda$new$0, a $ in the middle of a method name that has nothing to do with nesting. The attribute is the only place the relationship is recorded, which is exactly why it exists.
Now the other two attributes, from the same file, and this is where the sets diverge:
NestMembers on Inner.class -- 6 entries:
#27 Inner$E #29 Inner$Iface #31 Inner$Member
#33 Inner$Nested #18 Inner$1Local #7 Inner$1
NestHost on Inner$Nested.class -- ONE u2, no count:
0010 -> #16 = Inner
NestMembers lists all six, which is exactly the InnerClasses set. So for this class the two agree — and that is a coincidence of a small example, not a rule. The distinction is what each one is for: InnerClasses says how the compiler named things, NestMembers says who may read whose private fields. A class can be in a nest without being a nested class in the source sense, and the format has no requirement that the lists match.
And note the shape difference once more, because it is the previous concept's point made concrete: NestHost is a bare index with no count, while NestMembers is a counted list. A reader that assumed a common body shape would read NestHost's index as a count and walk into the following attribute. The raw bytes 00 10 are two bytes of body; there is no count anywhere in them.
What a nest actually buys, and why it replaced synthetic accessors. The source-level problem is old and the mechanism is not obvious:
That is a real optimisation with a real mechanism behind it, and the interesting part is what moved. The compiler no longer has to invent a method; the verifier now does the work, by consulting NestHost and NestMembers when it type-checks the getfield. The cost has not disappeared — it has moved from the class file's method table to its attribute list, and from run time to load time. That is the trade the format prefers throughout: pay at load, once, so the hot path stays direct.
And the two attributes are what make that possible from both sides. A getfield inside Inner needs to know that Inner belongs to Outer's nest — so Inner$Nested.class needs NestHost. And the verifier needs to know that Outer claims Inner as a member, rather than trusting a self-declaration — so Outer.class needs NestMembers. Both directions are required because either one alone would let a class claim access it has not been given.
That is the security argument, and it is worth making explicit because it is the same argument as control flow guard and weak externals in this collection: a claim about a relationship has to be checkable by someone who is not the claimant.NestMembers is the outer's side of that, and without it NestHost would be a class simply asserting that it may read private state. Compare a ELF symbol's binding and visibility, which is the same two-sided arrangement between a symbol and the section that defines it.
One consequence worth knowing before you read a real file: the nest is not the same as the enclosing class, and it is not transitive. A class in Outer's nest can read Inner's privates and vice versa, but a third class in the same nest that is a nested class of a different member is not automatically related. And a static nested class is in the nest like any other, because a static nested class still has access to the outer's privates in the source language. The Inner$E enum in this course's sample is nested, static, and a member of the nest — three facts from three different mechanisms, and only the third one is about access.
$ javap -v -p out/Inner.class
$ python3 courses/jvm/assets/samples/crosscheck.py classes/Inner.classInner.class's six InnerClasses records by hand. Count is 00 06 and each record is eight bytes. Then explain each of the three zero fields — which records have outer = 0, which have name = 0, and why the anonymous class has both.0x0608 and render it with the class table and with the member table. The class table calls 0x0008 unknown; the member table calls it ACC_STATIC, and the interface genuinely is static. That is the Module 2 finding arriving as a live bug in a real file.$1 counter. Add a second anonymous class to Inner and recompile. The new file will be Inner$2.class, and the local class declared after it will move to Inner$2Local — which shows the counter is shared, and is the most surprising naming fact in the format.InnerClasses from Inner.class, NestMembers from Inner.class, and NestHost from each nested class. Then deliberately break the correspondence — delete a NestMembers entry — and confirm the JVM rejects the class even though the InnerClasses records are untouched.--release 8 and with --release 17, and count the access$ methods. They disappear in the newer one, replaced by two attributes and a getfield — and the file gets smaller even though it gained an attribute.NestHost shape deliberately. Write a reader that assumes every attribute body is a counted list, and run it on a nested class. It will read the NestHost index as a count and walk into the following attribute, and the end-of-file assertion will be the only thing that catches it.Without looking back: a InnerClasses record has inner = #7, outer = 0, name = 0, flags = 0. What class is this, why does its file have a numeric component in its name, and what does the 0x0008 bit mean on a record whose other flags are 0x4018?
You are writing a tool that finds every class in a nest, given one class file. It reads InnerClasses from that file and follows each record's outer_class_info_index, which works for a static nested class and fails for both an anonymous class and a class declared inside a method: in both cases the outer index is zero, so the tool finds nothing and reports the class as having no nest. javap on the same file lists the nest correctly. What is the defect, what is the correct source for the relationship, and what is the general rule about trusting a field that is allowed to be zero?
The general habit: a field that is permitted to hold a sentinel value carries no information when it does, and a consumer that reads a sentinel as a value will invent a relationship that is not there. Check for the sentinel before you use the field.
A nest is a two-sided declaration, and this collection has three other mechanisms built the same way, for the same reason. An ELF symbol's binding and an object's visibility are recorded by the producer and checked by whoever consumes the reference. A PE's load config records guard metadata that the loader verifies. A COFF weak external is a claim about a symbol that the linker is expected to check rather than trust. The pattern is always the same: a claim about a relationship is only useful if something other than the claimant checks it, and a one-sided declaration is just an assertion.
The $ finding is a naming lesson the whole collection keeps teaching. A PE import is by ordinal, a COFF static-library symbol by ordinal in a string table, and both are positions that a producer fills in and a consumer resolves. The Java $ convention is the weakest version of the family — a name, not a position, and one the format does not require — which is why it is the one that needs an attribute to record the real relationship. Compare the ELF symbol table, where the name is in the file and the index is assigned by the linker: a position is resolved, a name is trusted, and only the second kind needs a second attribute to say what it means.
The synthetic-accessor story connects to COMDAT and to weak externals in a specific way: all three are compiler-generated members that exist only to satisfy a language rule the machine does not have. A nested class reading a private field is not a privilege the CPU understands; private is enforced by the compiler and the verifier, so the mechanism has to be a generated method or a declared relationship. The trend across all three is the same — move enforcement from generated code to checked metadata, and the direction of travel is set by the cost of generated code, which is bytes in the file and a call in the profile.
And the flag-table surprise is the third time this course has found it, which is the argument for teaching it as a general habit rather than a fact. A section header's flags and the file header's are different; a PE section's characteristics are a third vocabulary. Any format with more than one kind of record ends up with more than one flag table, because the flags describe the record and the records differ — and the failure is always a bit reported with a name that is meaningless in that position, which is a class of wrong that no reader can detect from the value alone.
Next: the Signature attribute, which is the second type grammar in the format — and the one place where the file says something the language deliberately erased.