7.4
Static Nested, Inner, Local, and Anonymous Classes
Choose the right nesting form by asking about scope, the enclosing object, and captured values.
Previously on
You know top-level classes, object references, static, inheritance, abstract classes, and interfaces.
This lesson changes where a class declaration appears. The important questions are not visual. Ask what scope the type needs, whether it needs an enclosing object, and which local values it captures.
The problem
Some helper types belong to exactly one outer class. A map entry or builder may have no useful meaning outside its owner.
Other implementations are needed in one method or one expression. Giving each one a top-level file expands the visible design without adding a reusable concept.
Java offers four nested forms:
| Form | Declared where | Automatic enclosing object |
|---|---|---|
| static nested class | member of a class, with static |
no |
| inner class | non-static member of a class | yes |
| local class | inside a block | depends on its context and use |
| anonymous class | inside an object-creation expression | depends on its context and use |
The correct choice follows from lifetime and access, not from which syntax is shortest.
The idea
Start with a static nested class
Use a static nested class when the helper belongs conceptually to the outer type but does not need one outer object.
class Registry {
private static int nextId = 1;
static class Entry {
private final int id;
private final String name;
Entry(String name) {
id = nextId++;
this.name = name;
}
String label() {
return id + ": " + name;
}
}
}Create it using the outer type as a qualifier:
Registry.Entry entry = new Registry.Entry("Atlas");
System.out.println(entry.label());No Registry object is required. Entry can use the outer class’s static field.
A static nested class may receive an outer object explicitly if one operation needs it. The key is that no enclosing instance arrives automatically.
Use an inner class when every helper belongs to one object
class Registry {
private final String region;
Registry(String region) {
this.region = region;
}
class Entry {
private final String name;
Entry(String name) {
this.name = name;
}
String label() {
return region + ": " + name;
}
}
}An Entry needs a particular Registry because label reads that registry’s region.
Registry north = new Registry("north");
Registry south = new Registry("south");
Registry.Entry a = north.new Entry("Atlas");
Registry.Entry b = south.new Entry("Tide");
System.out.println(a.label()); // north: Atlas
System.out.println(b.label()); // south: TideThe syntax north.new Entry(...) identifies the enclosing object. Inside a Registry instance method, new Entry(...) uses the current registry automatically.
Under the hood
Going deeperStatic nested does not mean “cannot access private code”
Both static nested and inner classes are members of the enclosing class. Java allows nested code and its enclosing class to access each other’s private members.
The difference is the receiver.
class Outer {
private static int shared = 10;
private int perObject = 20;
static class Nested {
int readShared() {
return shared;
}
int readObject(Outer outer) {
return outer.perObject;
}
}
}Nested accesses shared directly. It needs an explicit Outer reference for perObject because there is no automatic enclosing object.
An inner class can access both directly because its instance has an enclosing Outer.
Name shadowing and Outer.this
An inner method can have several values named name:
class Registry {
private String name = "outer";
class Entry {
private String name = "inner";
void print(String name) {
System.out.println(name); // parameter
System.out.println(this.name); // Entry field
System.out.println(Registry.this.name); // Registry field
}
}
}Registry.this means the enclosing Registry object. this means the current Entry object.
Avoid heavy shadowing in ordinary code. The syntax is valuable when reading libraries and generated code.
Inner classes and static members
Older Java rules restricted many static declarations inside inner classes. Current Java permits static members in inner classes.
That does not turn an inner class into a static nested class. The category depends on whether the class declaration itself has static and whether instances have an enclosing object.
Use the enclosing-instance rule as your definition. Version-specific static-member restrictions are not a reliable mental model.
The enclosing relationship can affect reachability
An inner-class instance is created with an enclosing instance. When its code uses that outer object, the compiled class needs a way to retain and reach it.
long-lived collection -> Entry inner object -> Registry outer objectIf the collection still reaches Entry, garbage collection must also treat the referenced Registry as reachable.
This is useful when Entry genuinely depends on registry state. It is wasted retention when the helper never needed that state.
Current javac versions can omit a stored outer reference when an inner class never uses the enclosing instance. That is an optimisation, not a design promise.
Adding one outer-field read can make the compiler restore the reference. A static nested declaration prevents that dependency from appearing later by accident.
Static nested classes have no automatic outer association. Local classes, anonymous classes, and lambdas can also retain objects they capture.
The general review question is: what objects can this small callback keep alive?
Local classes keep a type inside one block
A local class is declared inside a method, constructor, or another block:
static String normalise(String raw) {
class Cleaner {
String clean(String value) {
return value.trim().toLowerCase();
}
}
Cleaner cleaner = new Cleaner();
return cleaner.clean(raw);
}Only code after the declaration and within its scope can name Cleaner. The type does not become a member of the outer class.
This can group several methods and a little state for one algorithm. If the helper becomes large or reusable, move it to a member or top-level type.
Local classes can capture effectively final values
static Runnable greeter(String prefix) {
int times = 2;
class GreetingTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < times; i++) {
System.out.println(prefix);
}
}
}
return new GreetingTask();
}The returned object can run after greeter has returned. Its code still needs prefix and times.
Both variables are effectively final. They are assigned once and never reassigned. Writing the final keyword would not change their status.
This fails:
int times = 2;
times++;
class GreetingTask implements Runnable {
public void run() {
System.out.println(times); // compile-time error
}
}Java captures values, not a live local-variable slot in a vanished stack frame. Reassignment would make the two storage locations appear inconsistent.
Capturing a reference copies the reference value. The referenced object may still be mutable:
StringBuilder text = new StringBuilder("A");
class Appender {
void add() {
text.append("B"); // object mutation is allowed
}
}
// text = new StringBuilder(); // reassignment would break effective finalityAnonymous classes combine declaration and creation
An anonymous class has no source-level name. It extends one class or implements one interface in the object-creation expression.
interface Alarm {
void ring();
}
Alarm alarm = new Alarm() {
@Override
public void ring() {
System.out.println("Wake up");
}
};
alarm.ring();Read the expression in two parts:
- Declare an unnamed class that implements
Alarm. - Create one object of that unnamed class.
The semicolon ends the entire assignment expression after the anonymous class body.
An anonymous class can declare fields, methods, and instance initializer blocks. It cannot declare a normal constructor because there is no class name to use.
It can extend a class and pass constructor arguments:
Thread worker = new Thread("registry-worker") {
@Override
public void run() {
System.out.println(getName());
}
};The reference type controls which methods the caller can name. A new method declared only inside the anonymous class is not visible through an Alarm reference.
Anonymous classes follow capture rules similar to local classes. They can also use an enclosing instance when created in an instance context.
Lambda and anonymous class are related, not identical
For a functional interface with one abstract method, a lambda can supply that method’s behaviour:
Alarm alarm = () -> System.out.println("Wake up");This replaces the previous anonymous-class use case, but it does not declare an anonymous class in the Java language.
Important differences include:
| Anonymous class | Lambda |
|---|---|
| creates a new class body and object identity | represents behaviour for a target functional interface |
this means the anonymous object |
this keeps the enclosing meaning |
| can declare instance fields and extra methods | expression or block supplies one function body |
| may implement an interface with several abstract methods | requires a functional interface target |
JVMs commonly implement lambdas through invokedynamic and runtime linkage. They need not generate one ordinary anonymous class file for each lambda expression.
Phase XI covers target typing, capture, and method references. The preparation from this lesson is narrower: you know what an anonymous class is, so you can compare the two accurately.
Inspect what the compiler generated
Compile a file containing each nested form, then list its class files. Names often look like these:
Outer.class
Outer$Nested.class
Outer$Inner.class
Outer$1Local.class
Outer$1.classThe exact synthetic names are compiler details. Use javap -p to inspect fields and methods, and javap -c to inspect bytecode.
Generated names and fields help diagnose captures, but source semantics remain the contract. Do not write application logic that depends on a synthetic class name.
What it costs
Nesting can express ownership and reduce public surface area. Deeply nested logic is harder to test and scan.
Inner and captured objects can extend the lifetime of larger objects. Static nesting avoids the automatic enclosing reference.
Local and anonymous classes work well for short, local implementations. Promote them to named types when behaviour needs reuse, documentation, or independent tests.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
What separates a static nested class from a non-static inner class?
Show the answer
An inner-class instance is associated with an enclosing object and can use that object's instance members directly. A static nested class has no automatic enclosing-object reference. It can use instance state only through an explicit object reference.
Why may a local or anonymous class capture only final or effectively final local variables?
Show the answer
The created object may outlive the method call and its stack frame. Java captures the local variable's value for later use. Preventing reassignment keeps the method variable and captured value from appearing to be one changing variable when they are separate storage.
Is a lambda an anonymous class with shorter syntax?
Show the answer
No. Both can supply behaviour for an interface, so a lambda replaces many anonymous-class use cases. Their semantics differ: a lambda does not introduce a new meaning of
this, cannot declare instance state, and is commonly implemented without generating one anonymous class per expression.How can an inner class contribute to a memory leak?
Show the answer
An inner class that uses enclosing-instance state needs a reference to that object. If long-lived code stores the inner object, the enclosing object also remains reachable. A compiler may omit the stored reference when the inner class never uses it, but making the class static removes the dependency by design.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises100 pointsabout 110 minutes
Static Nested or Inner
ex-7-4-aTwo nested classes, one word apart, with completely different capabilities.
Start by getting both working. The inner class creation syntax is unusual, and working it out yourself is more useful than being shown, because the strangeness is the language telling you something real.
Then uncomment the line in Entry and explain the error properly. Not “static things cannot see instance things”, but why that particular question has no answer.
Finish with javap. With the course JDK, the generated field is named this$0. That name is a compiler detail. The important fact is that Auditor needs a reference to its enclosing object because it reads orgName.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Nested {
Entry(String name): String show()
Auditor: String report()
static String makeAuditorReport()
static boolean entryCanReadOuterInstanceField()
static String hiddenFieldName()
static boolean auditorHasHiddenField()
static boolean entryHasHiddenField()
static String defaultChoice()
}Entry stays static nested and Auditor stays inner, because the difference between them is the exercise. show returns the name, a space, v and the version. report includes both orgName and the version. makeAuditorReport has to build an outer object before it can build an Auditor. hiddenFieldName is the field name exactly as the course JDK's javac prints it.
What your program must do
- Create both kinds of nested object and get them working
- Uncomment the outer field access in Entry and explain the error
- Run javap on both nested classes and find the field the compiler added
- State which of the two you should reach for by default
public class Nested {
private String orgName = "Layers of Logic";
private static String version = "1.0";
static class Entry {
String name;
Entry(String name) { this.name = name; }
String show() {
return name + " v" + version;
// return name + " " + orgName; // uncomment: does this compile? why not?
}
}
class Auditor {
String report() {
return "auditing " + orgName + " v" + version;
}
}
// TODO: build a Nested, then an Auditor from it, and return the report.
// The syntax for creating an inner object is unusual. Work it out.
static String makeAuditorReport() {
return "TODO";
}
// ---- what you worked out ----
// TODO: can Entry read orgName?
static boolean entryCanReadOuterInstanceField() { return true; }
// TODO: compile, then run javap -p 'Nested$Auditor' and read the field list.
// What is the field called that you never wrote?
static String hiddenFieldName() { return "?"; }
// TODO
static boolean auditorHasHiddenField() { return false; }
// TODO: now run javap -p 'Nested$Entry' and compare
static boolean entryHasHiddenField() { return true; }
// TODO: one word. Which of the two should you reach for by default?
static String defaultChoice() { return "?"; }
public static void main(String[] args) {
System.out.println(new Entry("Atlas").show());
System.out.println(makeAuditorReport());
}
}
Hint 1
Nested n = new Nested(); Nested.Auditor a = n.new Auditor();. That syntax looks strange and it is telling you something true.Hint 2
Entry is static, so it has no outer object. orgName belongs to a particular Nested, and there is no particular Nested here, so the question has no answer and the compiler rejects it.Hint 3almost the answer
javap -p 'Nested$Auditor' and look at the field list. The compiler added one, and it is not a name you could have typed. Then run the same command on 'Nested$Entry' and see that it has nothing. That difference is the whole section.The Leak That Looks Tiny
ex-7-4-bFive small listener objects can keep much larger holders alive.
Run the inner version first and watch the memory climb. Nothing in the loop keeps a Holder. The only thing stored is a listener with no fields at all.
Then run the static version and watch the memory stay flat.
Explain the difference precisely. The listener that reads an outer field carries a route back to the holder. The static listener has no automatic enclosing-object dependency.
This is a genuine production bug shape. Event listeners, callbacks and adapters stored in long lived collections are exactly where it happens, and the fix is one keyword that nobody notices is missing.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Pin {
Holder(int megabytes, String label): String getLabel(), int bufferBytes()
Holder.UsingListener, Holder.BlindListener, Holder.StaticListener: String describe()
static String hiddenFieldName()
static boolean usingListenerPinsHolder(), blindListenerPinsHolder(), staticListenerPinsHolder()
static long recordedUsingMB(), static long recordedStaticMB()
static String oneKeywordFix()
static boolean addingOneLineCanCreateTheLeak()
}The three listener classes are given and must keep the shape they have, because the difference between them is the whole exercise. UsingListener reads an outer field, BlindListener is inner and reads nothing outer, StaticListener is static nested. The tests drop the Holder and ask the garbage collector whether it could take it, once for each listener. The recorded numbers are your own, from five 12 MB holders.
What your program must do
- Run all three loops and record the memory each one leaves behind
- Run javap on all three listener classes and compare their field lists
- Explain the difference in terms of what each listener holds
- Say what one keyword would remove the possibility of this bug
import java.util.*;
public class Pin {
static class Holder {
private final byte[] hugeBuffer;
private final String label;
Holder(int megabytes, String label) {
this.hugeBuffer = new byte[megabytes * 1_000_000];
this.label = label;
}
String getLabel() { return label; }
int bufferBytes() { return hugeBuffer.length; }
// Inner, and it reads an outer field.
class UsingListener {
String describe() { return "listener for " + label; }
}
// Inner, and it touches nothing outer. Predict what the compiler does with this one.
class BlindListener {
String describe() { return "listener for nobody"; }
}
// Static nested.
static class StaticListener {
String describe() { return "listener for nobody"; }
}
UsingListener makeUsing() { return new UsingListener(); }
BlindListener makeBlind() { return new BlindListener(); }
static StaticListener makeStatic() { return new StaticListener(); }
}
static long usedMB() {
Runtime rt = Runtime.getRuntime();
System.gc();
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return (rt.totalMemory() - rt.freeMemory()) / (1024 * 1024);
}
// ---- what you found ----
// TODO: the field name from the previous exercise
static String hiddenFieldName() { return "?"; }
// TODO: run the three loops, then run javap -p on each listener class
static boolean usingListenerPinsHolder() { return false; }
// TODO: this one is the surprise. Check it before you answer.
static boolean blindListenerPinsHolder() { return true; }
// TODO
static boolean staticListenerPinsHolder() { return true; }
// TODO: the megabytes you actually saw, in the using run and the static run
static long recordedUsingMB() { return 0; }
static long recordedStaticMB() { return 0; }
// TODO: one word, on the nested class
static String oneKeywordFix() { return "?"; }
// TODO: could a change that says nothing about memory create this leak?
static boolean addingOneLineCanCreateTheLeak() { return false; }
public static void main(String[] args) {
System.out.println("start : " + usedMB() + " MB");
List<Object> kept = new ArrayList<>();
for (int i = 0; i < 5; i++) {
kept.add(new Holder(12, "unit-" + i).makeUsing()); // keep only the tiny listener
}
System.out.println("5 using inner: " + usedMB() + " MB");
// TODO: repeat with makeStatic, and then with makeBlind, and compare all three
}
}
Hint 1
Holder goes out of scope immediately and only the listener is kept. So the question is always the same: what else is the listener holding on to?Hint 2
javap -p on all three. Current javac can omit the stored outer reference when the class never uses it. Treat that as observed compiler behaviour, not a language guarantee.Hint 3almost the answer
static takes the possibility away for good.Anonymous Class, Then Lambda
ex-7-4-cWrite the same behaviour three ways: a named class, an anonymous class, and a lambda.
Then compile it and look at the folder. The anonymous class produced a real file with a real name, one the compiler invented. Seeing Anonymous$1.class on disk is what turns “a class with no name” from a phrase into a fact.
Do the same for a sort, first with an anonymous Comparator and then with a lambda, and confirm the results match exactly.
The deliverable is the comparison. List what target typing lets the lambda omit, then state how this and generated class identity differ between the two forms.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Anonymous {
static Greeter namedGreeter(), anonymousGreeter(), lambdaGreeter()
static Comparator<String> anonymousLengthComparator(), lambdaLengthComparator()
static boolean anonymousClassProducesAClassFile(), lambdaProducesAClassFile()
static boolean lambdaInterfaceNameComesFromTheTarget(), lambdaMethodNameComesFromTheInterface(), lambdaParameterTypeIsInferred()
static int abstractMethodsALambdaTargetCanHave()
}All three greeters return "hello " and the name. The comparators sort shortest first. The form matters as much as the behaviour here, so the tests ask the runtime what each object actually is: the anonymous ones must report themselves as anonymous classes, and the lambdas must not.
What your program must do
- Write the same behaviour three ways, and call all three
- Compile it and list the class files produced
- Sort the same list with both comparators and confirm identical results
- Write down which source details target typing lets the lambda omit
- Explain two semantic differences between a lambda and an anonymous class
import java.util.*;
interface Greeter { String greet(String name); }
// TODO 1. a named class implementing Greeter
class NamedGreeter implements Greeter {
@Override public String greet(String name) { return "TODO"; }
}
public class Anonymous {
static Greeter namedGreeter() { return new NamedGreeter(); }
// TODO 2. the same behaviour using an anonymous class
static Greeter anonymousGreeter() {
return null;
}
// TODO 3. the same behaviour using a lambda
static Greeter lambdaGreeter() {
return null;
}
// TODO 4. sort by length, as an anonymous Comparator
static Comparator<String> anonymousLengthComparator() {
return null;
}
// TODO 5. the same comparison behaviour, as a lambda
static Comparator<String> lambdaLengthComparator() {
return null;
}
// ---- what the folder showed ----
// Compile, then list the class files in the folder.
// TODO
static boolean anonymousClassProducesAClassFile() { return false; }
// TODO
static boolean lambdaProducesAClassFile() { return true; }
// ---- what the lambda removed, and how the compiler already knew it ----
// TODO: the interface name is gone. Where did the compiler get it?
static boolean lambdaInterfaceNameComesFromTheTarget() { return false; }
// TODO: the method name is gone too
static boolean lambdaMethodNameComesFromTheInterface() { return false; }
// TODO: and the parameter type
static boolean lambdaParameterTypeIsInferred() { return false; }
// TODO: how many abstract methods can an interface have and still accept a lambda?
static int abstractMethodsALambdaTargetCanHave() { return 0; }
public static void main(String[] args) {
// TODO: call all three greeters and print the class each one really is
List<String> units = new ArrayList<>(List.of("Cipher", "Atlas", "Drift", "Beacon"));
// TODO: sort a copy with each comparator and confirm the results match
if (units.isEmpty()) System.out.println();
}
}
Hint 1
new Greeter() { public String greet(String name) { ... } }. When you assign it, note the semicolon after the closing brace: it is an expression, not a declaration.Hint 2
Anonymous$1.class and probably $2. Those are your anonymous classes. The lambda adds no ordinary class file for the expression because the JVM links its implementation at run time.Hint 3almost the answer
public, the braces and the return for this expression body. That shorter source does not make it an anonymous class.Why Effectively Final
ex-7-4-dA task that outlives the method that created it, still printing that method’s local variables.
Run it first and notice the strange part: by the time run() executes, the stack frame holding counter and label is long gone. Something must have been copied.
Then uncomment the increment and read the error. Note that it is reported on the line using the variable, not the line changing it, which is a hint about what Java is protecting.
Explain the rule yourself, in terms of stack frames and the heap. Do not quote the error message back. If your explanation mentions where each thing lives and when it disappears, you have it.
Finish by making a genuinely changing counter work. There is more than one way, and all of them involve moving the counter somewhere that outlives the frame.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Effectively {
static Task makeTask()
static Task makeCountingTask()
static boolean errorIsReportedOnTheUsingLine()
static String whereLocalsLive()
static String whereTheObjectLives()
static boolean valuesAreCopiedIntoTheObject()
static boolean aChangingLocalWouldMakeTwoAnswers()
}Task returns a String rather than printing, so the tests can read it. makeTask keeps the locals it has and returns "task 0". makeCountingTask returns a task whose first call gives "1", second "2", and so on, and two tasks made separately must count separately, so a static counter will fail. The two one word answers are stack or heap, case and spaces ignored.
What your program must do
- Run it and confirm the task works after makeTask has returned
- Uncomment the increment and record which line the error is reported on
- Explain the rule in terms of stack frames and the heap
- Make a genuinely changing counter that the task can see, and keep two of them independent
interface Task { String run(); }
public class Effectively {
static Task makeTask() {
int counter = 0;
String label = "task";
Task t = new Task() {
@Override public String run() {
return label + " " + counter;
}
};
// counter++; // uncomment: what happens, and on which line is it reported?
return t;
}
// TODO: a task that counts. Every call returns the next number, starting at 1,
// as a String. Two tasks made separately must count separately.
static Task makeCountingTask() {
int counter = 0;
return () -> String.valueOf(counter);
}
// ---- what you worked out ----
// TODO: was the error on the line that READS counter, or the line that CHANGES it?
static boolean errorIsReportedOnTheUsingLine() { return false; }
// TODO: one word. "stack" or "heap"
static String whereLocalsLive() { return "?"; }
// TODO: one word. Where does the task object live?
static String whereTheObjectLives() { return "?"; }
// TODO: how did run() still have the values after makeTask returned?
static boolean valuesAreCopiedIntoTheObject() { return false; }
// TODO: why is copying only safe when the original cannot change?
static boolean aChangingLocalWouldMakeTwoAnswers() { return false; }
public static void main(String[] args) {
Task t = makeTask();
System.out.println(t.run()); // makeTask has already returned by now
// TODO: run the counting task three times and check the number moves
}
}
Hint 1
t.run() happens. makeTask has already returned, so its frame is gone, and the task can still print label and counter.Hint 2
Hint 3almost the answer
AtomicInteger. The reference never changes, so it is effectively final, and what it holds can change as much as it likes. Make it inside the method, not as a static field, or two tasks would share it.After the credits
Choose a nested form with three questions:
- Does the helper belong to one outer type?
- Does each helper object need one enclosing object?
- Is the type needed outside this block or expression?
Those answers select static nested, inner, local, or anonymous without memorising four unrelated syntaxes.
Threads you opened in this section
- Anonymous classA lambda is this, with all the noise removed. Learn this and lambdas are free.11.1 - Lambdas and Functional Interfaces
Inner class will return in Phase X. The Collections Framework