Inner Classes
An inner class is a non-static class declared inside another class. Unlike a static nested class, every instance of an inner class is tied to an instance of the enclosing (outer) class, and it can use that outer object's fields and methods directly, including private ones. Inner classes are the right tool when a helper object must work with one specific outer object's state: an iterator over a collection, a view of part of a data structure, or a handler that reacts on behalf of its owner.
Declaring and Creating an Inner Class
public class Classroom {
private final String name;
private final List<String> students = new ArrayList<>();
public Classroom(String name) {
this.name = name;
}
public void enroll(String student) {
students.add(student);
}
public class Register { // inner class: no static
private int called = 0;
public String callNext() {
if (called >= students.size()) { // uses the outer object's list
return "Everyone in " + name + " has been called";
}
return students.get(called++) + " (" + name + ")";
}
}
}
Classroom java = new Classroom("Java-A");
java.enroll("Ada");
java.enroll("Linus");
Classroom.Register register = java.new Register(); // created from an outer object
System.out.println(register.callNext()); // Ada (Java-A)
System.out.println(register.callNext()); // Linus (Java-A)
System.out.println(register.callNext()); // Everyone in Java-A has been called
Register reads students and name as if they were its own fields. Those names refer to the fields of the specific Classroom the register was created from: here, java.
The outerObject.new syntax
Because an inner object needs an outer object, it is created through one:
Classroom.Register r = java.new Register();
Inside an instance method of the outer class, this is used implicitly, so a plain new Register() works:
public Register startRegister() {
return new Register(); // same as this.new Register()
}
From a static context, such as main, there is no this, so new Register() alone fails:
error: non-static variable this cannot be referenced from a static context
The Hidden Reference to the Outer Object
The compiler gives every inner object a hidden field pointing to its outer object. Conceptually, Register is compiled into something like:
class Classroom$Register {
final Classroom this$0; // reference to the outer object
Classroom$Register(Classroom outer) { this.this$0 = outer; }
// students means this$0.students
}
This is what makes direct access to students possible, and it has two consequences:
- Each inner object is bound to exactly one outer object. Two registers created from two classrooms operate on two different student lists.
- The inner object keeps its outer object alive. As long as a
Registeris reachable, itsClassroomcannot be garbage collected, even if nothing else refers to the classroom.
The second point matters for long-lived inner objects. An inner-class listener registered with a global event bus, or an inner object stored in a static cache, keeps its entire outer object in memory, which is a classic source of memory leaks in Java GUI and Android code.
Referring to the Outer Object: Outer.this
If an inner class has a member with the same name as the outer class's, the inner one shadows it. OuterClassName.this reaches the outer object:
public class Course {
private final String title = "Java Basics";
public class Module {
private final String title;
public Module(String title) {
this.title = title;
}
public String path() {
return Course.this.title + " > " + this.title;
}
}
}
Course c = new Course();
Course.Module m = c.new Module("Loops");
System.out.println(m.path()); // Java Basics > Loops
this.title is the module's; Course.this.title is the enclosing course's.
Inner Classes and Private Access
An inner class can read and write the outer class's private fields and call its private methods; the outer class can likewise access the inner class's private members. They are treated as parts of one unit:
public class Counter {
private int count;
private void bump() { count++; }
public class Clicker {
private int clicks; // private, yet visible to Counter
public void click() {
clicks++;
bump(); // calls the outer object's private method
}
}
public int clicksOf(Clicker c) {
return c.clicks; // outer reads the inner's private field
}
}
Classic Use: Iterators
The best-known use of inner classes is implementing Iterator, because an iterator must walk through one specific collection's internal storage:
import java.util.Iterator;
import java.util.NoSuchElementException;
public class RingBuffer implements Iterable<Integer> {
private final int[] data;
private int start = 0, size = 0;
public RingBuffer(int capacity) {
data = new int[capacity];
}
public void add(int value) {
int end = (start + size) % data.length;
data[end] = value;
if (size < data.length) {
size++;
} else {
start = (start + 1) % data.length; // overwrite the oldest value
}
}
@Override
public Iterator<Integer> iterator() {
return new RingIterator();
}
private class RingIterator implements Iterator<Integer> {
private int visited = 0;
@Override
public boolean hasNext() {
return visited < size; // outer's size
}
@Override
public Integer next() {
if (!hasNext()) throw new NoSuchElementException();
return data[(start + visited++) % data.length]; // outer's data and start
}
}
}
RingBuffer recent = new RingBuffer(3);
for (int v : new int[]{10, 20, 30, 40, 50}) {
recent.add(v);
}
for (int v : recent) {
System.out.print(v + " "); // 30 40 50
}
RingIterator is private, so the outside world sees only Iterator<Integer>. It reads data, start, and size from the specific buffer it was created for, which is exactly the relationship an inner class models. Each call to iterator() creates a fresh, independent iterator over the same buffer. The JDK's own collections, such as ArrayList, implement their iterators in the same way.
Local Classes
A local class is declared inside a method body. It is visible only within that block, and it can use the method's local variables, provided they are effectively final (never reassigned):
public static List<String> validate(List<String> emails, String requiredDomain) {
class Check { // local class
final String email;
Check(String email) { this.email = email; }
boolean ok() {
return email.contains("@") && email.endsWith("@" + requiredDomain);
}
}
List<String> invalid = new ArrayList<>();
for (String e : emails) {
if (!new Check(e).ok()) {
invalid.add(e);
}
}
return invalid;
}
System.out.println(validate(List.of("a@gocourse.in", "b@gmail.com", "nope"), "gocourse.in"));
// [b@gmail.com, nope]
Check uses requiredDomain, a parameter of the enclosing method. Java copies the value into the local class, which is why the variable must not change afterward. Local classes are uncommon in modern code; lambdas, records, and private methods usually express the same idea more simply. Local records, enums, and interfaces (Java 16+) are useful for small, method-specific data types.
Inner Class Restrictions
- An inner class instance cannot exist without an outer instance.
- Since Java 16, inner classes may declare
staticmembers (fields, methods, and nested types). Before Java 16, they could only declarestatic finalconstants. - Local variables captured by local and anonymous classes must be effectively final.
Inner Versus Static Nested: Choosing
Ask one question: does each object of the nested class need to work with a particular outer object's state?
| If... | Use |
|---|---|
| It reads or modifies the outer object's fields (iterators, views, cursors) | Inner class |
| It is independent data or a helper (builders, entries, results) | Static nested class |
| You are unsure | Static nested class, adding an explicit reference only if needed |
Making a nested class static by default avoids the hidden reference and its memory cost. An inner class should be a deliberate choice.
Practical Example: Bank Statement View
import java.util.ArrayList;
import java.util.List;
public class Account {
private final String owner;
private final List<Double> transactions = new ArrayList<>();
public Account(String owner) { this.owner = owner; }
public void post(double amount) { transactions.add(amount); }
public Statement statement(int lastN) {
return new Statement(lastN);
}
public class Statement {
private final int lastN;
private Statement(int lastN) {
this.lastN = lastN;
}
public void print() {
int from = Math.max(0, transactions.size() - lastN);
double opening = 0;
for (int i = 0; i < from; i++) {
opening += transactions.get(i);
}
System.out.println("Statement for " + owner);
System.out.printf(" Opening balance: %10.2f%n", opening);
double running = opening;
for (int i = from; i < transactions.size(); i++) {
double t = transactions.get(i);
running += t;
System.out.printf(" %-8s %10.2f → %10.2f%n",
t >= 0 ? "Credit" : "Debit", Math.abs(t), running);
}
}
}
public static void main(String[] args) {
Account acc = new Account("Ada");
acc.post(5000);
acc.post(-1200);
acc.post(2500);
acc.post(-300);
Account.Statement s = acc.statement(2);
s.print();
acc.post(1000); // a later transaction
System.out.println();
s.print(); // the same Statement sees the new data
}
}
Output:
Statement for Ada
Opening balance: 3800.00
Credit 2500.00 → 6300.00
Debit 300.00 → 6000.00
Statement for Ada
Opening balance: 6300.00
Debit 300.00 → 6000.00
Credit 1000.00 → 7000.00
Statement is an inner class because it is a live view of one account's data: it reads owner and transactions directly from the Account it came from. Its constructor is private, so statements can be created only through acc.statement(...), which supplies the outer object automatically. When a new transaction is posted, the existing Statement reflects it on the next print(), because it holds a reference to the account rather than a copy of its data.