Skip to main content
Java · lesson 60 of 66
Open the Java page →
Java intermediate Lesson 60 of 66

Inner and Anonymous Classes in Java

Master nested classes, local classes, and anonymous classes — when to use them for event handlers, callbacks, and encapsulating helper logic.

By SkillByExample · Updated

Java lets you declare a class inside another class, or even inside a method. There are four kinds: static nested, inner (non-static), local and anonymous. They differ in one thing that matters: whether they carry a reference to an enclosing object. Each example is a single file run with the JDK 21 source launcher (java File.java).

Static nested classes

A static nested class is an ordinary class that lives in another class’s namespace. It has no outer instance, so it can’t touch the outer class’s instance fields.

public class VehicleDemo {
    public static void main(String[] args) {
        Vehicle.Registration reg = new Vehicle.Registration("ABC-123");
        reg.print();
    }
}

class Vehicle {
    private String make = "Toyota";

    static class Registration {
        private final String number;
        Registration(String number) { this.number = number; }
        void print() {
            System.out.println("Reg: " + number);
            System.out.println("Make: " + make);
        }
    }
}
$ java VehicleDemo.java
VehicleDemo.java:16: error: non-static variable make cannot be referenced from a static context
            System.out.println("Make: " + make);
                                          ^
1 error
error: compilation failed

new Vehicle.Registration(...) needs no Vehicle, which is the point — and also why make, which belongs to some particular Vehicle, isn’t reachable. Remove that line and it prints Reg: ABC-123. Map.Entry is a static nested interface in the JDK.

Inner classes

A non-static nested class is created from an outer instance and can use its fields directly.

public class AccountDemo {
    public static void main(String[] args) {
        BankAccount account = new BankAccount(100);
        BankAccount.Transaction tx = account.new Transaction();
        tx.deposit(50);
        BankAccount.Transaction tx2 = account.new Transaction();
        tx2.deposit(25);
        System.out.println("balance seen by account: " + account.balance());
    }
}

class BankAccount {
    private double balance;
    BankAccount(double balance) { this.balance = balance; }
    double balance() { return balance; }

    class Transaction {
        void deposit(double amount) {
            balance += amount;                    // the outer object's private field
            System.out.println("Deposited " + amount + ", balance " + balance);
        }
    }
}
$ java AccountDemo.java
Deposited 50.0, balance 150.0
Deposited 25.0, balance 175.0
balance seen by account: 175.0

account.new Transaction() ties each transaction to account, so both write to the same private balance. Inside Transaction, balance is shorthand for BankAccount.this.balance.

Shadowing and Outer.this

When an inner class declares a name that the outer class also uses, the nearest one wins. Outer.this reaches past it.

public class Shadowing {
    private String name = "outer field";

    class Inner {
        private String name = "inner field";

        void print(String name) {
            System.out.println(name);
            System.out.println(this.name);
            System.out.println(Shadowing.this.name);
        }
    }

    public static void main(String[] args) {
        new Shadowing().new Inner().print("parameter");
    }
}
$ java Shadowing.java
parameter
inner field
outer field

Three variables called name, three ways to reach them: the parameter, the inner object’s field via this, and the outer object’s field via Shadowing.this.

Local classes

A local class is declared inside a method and is visible only there. It can read the method’s local variables if they are effectively final.

import java.util.ArrayList;
import java.util.List;

public class LocalClass {
    static List<String> validate(List<String> emails, int maxLength) {
        class Check {
            final String email;
            Check(String email) { this.email = email; }
            String problem() {
                if (!email.contains("@")) return "missing @";
                if (email.length() > maxLength) return "longer than " + maxLength;
                return null;
            }
        }
        List<String> problems = new ArrayList<>();
        for (String e : emails) {
            String p = new Check(e).problem();
            if (p != null) problems.add(e + ": " + p);
        }
        return problems;
    }

    public static void main(String[] args) {
        System.out.println(validate(List.of("a@b.io", "nope", "very.long.name@example.com"), 15));
    }
}
$ java LocalClass.java
[nope: missing @, very.long.name@example.com: longer than 15]

Check reads maxLength, a parameter of the enclosing method. A local class keeps a helper right next to its only use instead of cluttering the outer class.

Anonymous classes

An anonymous class declares and instantiates a one-off implementation in a single expression. Unlike a lambda, it can implement an interface with several methods and hold its own state.

public class Anonymous {
    interface KeyHandler {
        void onPress(char key);
        void onRelease(char key);
    }

    public static void main(String[] args) {
        KeyHandler handler = new KeyHandler() {
            private int presses = 0;                       // state of its own
            public void onPress(char key)   { presses++; System.out.println("pressed " + key + " (" + presses + ")"); }
            public void onRelease(char key) { System.out.println("released " + key); }
        };
        handler.onPress('A');
        handler.onRelease('A');
        handler.onPress('B');
        System.out.println(handler.getClass().getName());
    }
}
$ java Anonymous.java
pressed A (1)
released A
pressed B (2)
Anonymous$1

KeyHandler has two abstract methods, so no lambda could implement it. The compiler names the class Anonymous$1 — the first anonymous class inside Anonymous.

Captured variables must be effectively final

Anonymous classes, local classes and lambdas copy the local variables they use. If the variable could change afterwards, the copy would be stale, so the compiler forbids it.

public class Capture {
    public static void main(String[] args) {
        int clicks = 0;
        Runnable r = new Runnable() {
            public void run() { System.out.println("clicks = " + clicks); }
        };
        clicks++;
        r.run();
    }
}
$ java Capture.java
Capture.java:5: error: local variables referenced from an inner class must be final or effectively final
            public void run() { System.out.println("clicks = " + clicks); }
                                                                 ^
1 error
error: compilation failed

The clicks++ after the class makes clicks not effectively final. To share a changing count, keep it in an object — an int[] cell, an AtomicInteger, or a field — whose reference never changes:

import java.util.concurrent.atomic.AtomicInteger;

public class CaptureFixed {
    public static void main(String[] args) {
        AtomicInteger clicks = new AtomicInteger();
        Runnable click = () -> System.out.println("clicks = " + clicks.incrementAndGet());
        click.run();
        click.run();
    }
}
$ java CaptureFixed.java
clicks = 1
clicks = 2

this in an anonymous class vs a lambda

public class ThisDemo {
    private final String name = "ThisDemo instance";

    void run() {
        Runnable anon = new Runnable() {
            public void run()        { System.out.println("anonymous: " + this); }
            public String toString() { return "the anonymous object"; }
        };
        Runnable lambda = () -> System.out.println("lambda:    " + this);
        anon.run();
        lambda.run();
    }

    public String toString() { return name; }

    public static void main(String[] args) { new ThisDemo().run(); }
}
$ java ThisDemo.java
anonymous: the anonymous object
lambda:    ThisDemo instance

An anonymous class is a new object, so this is that object. A lambda is not a new scope for this; it means the enclosing instance. This is one reason to prefer lambdas for callbacks that mainly call back into the outer object.

Practice

  1. Give Pizza a static nested Builder so you can write new Pizza.Builder().cheese().topping("olives").build().
Solution
import java.util.ArrayList;
import java.util.List;

public class PizzaDemo {
    public static void main(String[] args) {
        System.out.println(new Pizza.Builder().cheese().topping("olives").topping("basil").build());
        System.out.println(new Pizza.Builder().build());
    }
}

class Pizza {
    private final boolean cheese;
    private final List<String> toppings;
    private Pizza(Builder b) { cheese = b.cheese; toppings = List.copyOf(b.toppings); }
    public String toString() { return "Pizza{cheese=" + cheese + ", toppings=" + toppings + "}"; }

    static class Builder {
        private boolean cheese;
        private final List<String> toppings = new ArrayList<>();
        Builder cheese()               { cheese = true; return this; }
        Builder topping(String t)      { toppings.add(t); return this; }
        Pizza build()                  { return new Pizza(this); }
    }
}
$ java PizzaDemo.java
Pizza{cheese=true, toppings=[olives, basil]}
Pizza{cheese=false, toppings=[]}

Builder is static because it builds pizzas that don’t exist yet — it needs no outer Pizza. It can still call Pizza’s private constructor, because nested classes share access with their enclosing class.

  1. Write an inner class Playlist.Cursor with next() that walks the outer playlist’s songs and wraps around at the end.
Solution
import java.util.List;

public class PlaylistDemo {
    public static void main(String[] args) {
        Playlist p = new Playlist(List.of("Intro", "Verse", "Outro"));
        Playlist.Cursor c = p.new Cursor();
        for (int i = 0; i < 5; i++) System.out.print(c.next() + " ");
        System.out.println();
    }
}

class Playlist {
    private final List<String> songs;
    Playlist(List<String> songs) { this.songs = songs; }

    class Cursor {
        private int pos = 0;
        String next() { return songs.get(pos++ % songs.size()); }
    }
}
$ java PlaylistDemo.java
Intro Verse Outro Intro Verse 

Cursor reads songs from its outer Playlist; two cursors on the same playlist would move independently because each has its own pos.

  1. Sort ["kiwi", "Banana", "apple"] case-insensitively three ways: an anonymous Comparator, a lambda, and a method reference.
Solution
import java.util.*;

public class ThreeComparators {
    public static void main(String[] args) {
        List<String> a = new ArrayList<>(List.of("kiwi", "Banana", "apple"));
        a.sort(new Comparator<String>() {
            public int compare(String x, String y) { return x.compareToIgnoreCase(y); }
        });
        List<String> b = new ArrayList<>(List.of("kiwi", "Banana", "apple"));
        b.sort((x, y) -> x.compareToIgnoreCase(y));
        List<String> c = new ArrayList<>(List.of("kiwi", "Banana", "apple"));
        c.sort(String::compareToIgnoreCase);
        System.out.println(a + " " + b + " " + c);
    }
}
$ java ThreeComparators.java
[apple, Banana, kiwi] [apple, Banana, kiwi] [apple, Banana, kiwi]

Comparator has one abstract method, so all three forms work; the lambda and method reference are the same behaviour with less code.

  1. Count anonymous button clicks without AtomicInteger, using a field on the anonymous class itself.
Solution
public class SelfCounting {
    interface Button { void click(); int count(); }

    public static void main(String[] args) {
        Button ok = new Button() {
            private int clicks;
            public void click() { clicks++; }
            public int count()  { return clicks; }
        };
        ok.click(); ok.click(); ok.click();
        System.out.println("clicked " + ok.count() + " times");
    }
}
$ java SelfCounting.java
clicked 3 times

A field of the anonymous object can change freely; only captured local variables must be effectively final.

Frequently Asked Questions

What's the difference between a nested class and an inner class?
An inner class is a non-static nested class — it has an implicit reference to its enclosing instance. A static nested class does not. Use inner classes when the nested class needs access to the outer instance; use static nested classes when it's just a namespace helper.
When do I use an anonymous class vs a lambda?
A lambda can only implement a functional interface — one with a single abstract method, such as Runnable or Comparator. Use an anonymous class when the interface has several abstract methods, when you need to extend a class, or when the object needs its own fields. Inside an anonymous class, this refers to the anonymous object; inside a lambda, it refers to the enclosing instance.
Can a local class access variables from its enclosing method?
Yes, but only final or effectively final variables — the same restriction as anonymous classes and lambdas. The compiler copies the value into the class when it is created, so the variable must never change afterwards.
Why can an inner class cause a memory leak?
Every inner class instance keeps a hidden reference to the outer object that created it. If the inner object lives longer — stored in a cache, a static field or a listener list — it keeps the whole outer object alive too. Make nested classes static unless they genuinely need the outer instance.