JAVA / NESTED CLASSES AND THE OBJECT CONTRACT
Inner classes and their link to the outer instance
Create inner classes bound to one enclosing instance, use outer.new Inner() and Outer.this, and see why the hidden link keeps the outer object alive.
What you will learn
- Create inner instances with outer.new Inner() when no enclosing this is in scope
- Reach shadowed outer state with Outer.this.field instead of this.field
- Explain the hidden enclosing-instance parameter added to inner-class constructors
- Spot memory retained because a stored inner instance pins its outer object
Understanding Inner classes and their link to the outer instance
A nested class that is not declared static is a member of the enclosing object, not merely of the enclosing type. When you write new Inner() the compiler quietly passes the current enclosing instance as a hidden first constructor argument and stores it in a synthetic final field. Every unqualified reference to an outer field or method inside the inner class body is then compiled into a lookup through that field, which is why an inner class can read and mutate the outer object's private state without parameters or getters.
Because the link arrives as a constructor argument, an inner class can only be created where an enclosing instance is available. Inside an instance method of the outer class this supplies it, so a plain new Inner() works; from a static method or from another class you must name the object, as in outer.new Inner(). When a name in the inner class is shadowed by a parameter, a local variable, or an inner field, Outer.this.name reaches past the shadow, and Outer.this on its own hands you the enclosing object as a value.
The link is fixed when the inner object is constructed, can never be reassigned, and counts as an ordinary strong reference. One outer object can have many inner instances, but each inner instance belongs to exactly one outer object for its whole life. That coupling is the reason to make a nested class inner at all, as with a cursor over one collection or a node in one tree, and it is also the failure mode: put an inner instance in a listener list or a cache and the entire outer object stays reachable until you remove it, which shows up as growing retained memory rather than as a compile error.
public class Thermostat {
private final String room;
private int target = 20;
Thermostat(String room) {
this.room = room;
}
class Dial { // inner: no static keyword
private final int step;
Dial(int step) {
this.step = step;
}
void turnUp() {
target += step; // target belongs to the enclosing Thermostat
}
String label() {
return room + " -> " + target;
}
Thermostat owner() {
return Thermostat.this; // the enclosing instance as a value
}
}
public static void main(String[] args) {
Thermostat kitchen = new Thermostat("kitchen");
Thermostat study = new Thermostat("study");
Dial coarse = kitchen.new Dial(5); // main is static, so name the outer object
Dial fine = kitchen.new Dial(1);
Dial studyDial = study.new Dial(5);
coarse.turnUp();
fine.turnUp();
studyDial.turnUp();
System.out.println(coarse.label());
System.out.println(fine.label());
System.out.println(studyDial.label());
System.out.println(coarse.owner() == fine.owner());
System.out.println(coarse.owner() == studyDial.owner());
}
}An inner-class object is bound at construction to exactly one enclosing object, and every access to outer state travels through that hidden reference.
Worked examples
Three levels of the same name
Shows how a parameter, an inner field, and an outer field with identical names are told apart.
public class Outer {
private String tag = "outer";
class Inner {
private String tag = "inner";
void show(String tag) {
System.out.println(tag);
System.out.println(this.tag);
System.out.println(Outer.this.tag);
}
}
public static void main(String[] args) {
Outer o = new Outer();
Inner in = o.new Inner();
in.show("parameter");
}
}Example explained
Line 1The bare name tag resolves to the innermost declaration in scope, which is the method parameter.
Line 2this inside Inner is the Inner object, so this.tag is the inner field, never the outer one.
Line 3Outer.this.tag follows the hidden link to the enclosing object and reads its field.
Line 4Without the Outer.this qualifier the outer tag is unreachable here, since both closer names hide it.
A cursor that stays attached to its bag
Demonstrates that an inner instance stores the outer object itself, not a snapshot of its data.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Bag implements Iterable<String> {
private final List<String> items = new ArrayList<>();
void add(String s) {
items.add(s);
}
@Override
public Iterator<String> iterator() {
return new Cursor();
}
class Cursor implements Iterator<String> {
private int i = 0;
@Override
public boolean hasNext() {
return i < items.size();
}
@Override
public String next() {
return items.get(i++);
}
}
public static void main(String[] args) {
Bag bag = new Bag();
bag.add("a");
Iterator<String> it = bag.iterator();
bag.add("b");
while (it.hasNext()) {
System.out.println(it.next());
}
}
}Example explained
Line 1new Cursor() needs no qualifier because the enclosing instance is the this of iterator().
Line 2hasNext reads items.size() through the stored link on every call, so it sees the element added after the cursor was made.
Line 3Cursor keeps no copy of the list; it keeps the Bag, which is why the two stay in step.
Line 4A second Bag's items are invisible to this cursor: the link was fixed when new Cursor() ran.
Looking at the hidden link
Uses reflection to show the enclosing instance as a real constructor parameter and a real field.
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
public class Ledger {
private final String id = "L-1";
class Entry {
int amount = 5;
String describe() {
return id + ":" + amount;
}
}
public static void main(String[] args) throws Exception {
Constructor<?> ctor = Entry.class.getDeclaredConstructors()[0];
System.out.println(ctor.getParameterCount());
System.out.println(ctor.getParameterTypes()[0].getSimpleName());
Field link = Entry.class.getDeclaredField("this$0");
System.out.println(link.isSynthetic());
Ledger ledger = new Ledger();
Entry entry = ledger.new Entry();
link.setAccessible(true);
System.out.println(link.get(entry) == ledger);
System.out.println(entry.describe());
}
}Example explained
Line 1Entry declares no constructor, yet the generated one takes one parameter, and its type is Ledger.
Line 2The field this$0 is marked synthetic, meaning the compiler invented it rather than the source declaring it.
Line 3Reading that field gives back the very object used in ledger.new Entry(), so the link is plain reference identity.
Line 4describe() resolves id through this$0, which is why an unqualified outer field works inside Entry.
Important notes
The name this$0 is javac's convention, not part of the language, so it is useful in a debugger or heap dump but should never be referenced by production code.
Since Java 16 an inner class may declare static members; earlier releases allowed only static final constants. The enclosing-instance requirement is unchanged either way.
Common mistakes
Calling new Inner() from main: it does not compile because no enclosing instance exists, and adding static to silence the error quietly removes all access to the outer object's fields.
Writing new Outer.Inner(): rejected by the compiler, because new must follow the enclosing instance, as in outer.new Inner().
Using this.count for an outer field: this is the inner object, so the code either fails to compile or silently reads a same-named inner field instead of Outer.this.count.
Try it yourself
Change, predict, then run
Write a Cart class with a private int total and an inner class Item whose constructor adds its price to the enclosing cart's total, then create one cart, add two items with cart.new Item(...), and print the total. Now give Item its own int total field and fix the constructor with Cart.this.total so the cart still adds up correctly.
Open the Java workspaceCheck your understanding
A Tree class creates its inner class Node with new Node() inside the instance method add(). You move that code into a static factory method static Tree of(...), and new Node() stops compiling. What is going on?
- A static method has no this to supply the enclosing instance, so the call must become treeRef.new Node()
- Node must be declared public before a static method is allowed to construct it
- Adding an explicit Node() {} constructor restores the no-argument form and fixes the error
- Marking Node static is the minimal fix and leaves behaviour identical
Show answer
An inner-class constructor always needs an enclosing instance, which an instance method provides through this and a static method cannot provide at all, so the object has to be named explicitly. Option 3 is tempting because it does compile, but it is not behaviour preserving: once Node is static, every reference it makes to Tree's instance fields breaks and Node objects no longer belong to a particular tree. Option 2 changes nothing, since the compiler still prepends the hidden Tree parameter to whatever constructor you declare.