Inner Classes

Su Suriya Ravichandran Updated 14 Nov 2026
8 min read ·Lesson 60 of 133

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:

  1. Each inner object is bound to exactly one outer object. Two registers created from two classrooms operate on two different student lists.
  2. The inner object keeps its outer object alive. As long as a Register is reachable, its Classroom cannot 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 static members (fields, methods, and nested types). Before Java 16, they could only declare static final constants.
  • 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 unsureStatic 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.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about Inner Classes and get answers drawn from it.

Signed-in readers only.