Tutorials

The Ultimate Guide to Nested Classes in Java

Key Takeaways

  • Use Java nested classes to group closely related types and hide implementation details.
  • A static nested class has no implicit enclosing instance; a non-static member inner class is associated with a particular enclosing object.
  • Local and anonymous classes can capture enclosing local variables only when those variables are final or effectively final.
  • Since Java 16, inner classes can declare static fields, methods, and initializers—not just constant variables.
  • Prefer static member classes unless the enclosing-instance relationship is intentional; prefer lambdas for simple functional-interface implementations.

Java nested classes are classes declared inside another class or interface. They organize types that belong together and let you keep implementation details close to the code that uses them. The key design choice is whether the nested object needs an implicit connection to an enclosing object.

This guide covers static member classes, non-static member inner classes, local classes, and anonymous classes, then explains capture, shadowing, and when a lambda is the better choice.

Running the Examples

The complete examples target Java 25 without preview features. Each is independent: save one as MainClass.java, replacing the previous example, and run these commands with JDK 25:

javac --release 25 MainClass.java
java MainClass

The javac documentation explains how --release selects language rules, target class files, and available platform APIs. The java launcher documentation covers running the compiled class.

What Is a Nested Class?

A member class is declared directly in a class or interface body, alongside fields and methods. A local class is declared in a block, such as a method body. An anonymous class has no declared name and is commonly created within a new expression.

An inner class is a nested class that is neither explicitly nor implicitly static. That includes non-static member classes, ordinary local classes, and anonymous classes. Nested records and enums are implicitly static, so they aren't inner classes. These distinctions follow the Java Language Specification's class terminology.

The Four Forms at a Glance

FormDeclaration locationImplicit enclosing instanceTypical reason to choose it
Static nested classMember scopeNoA named helper or related public type
Member inner classMember scope, without staticYesBehavior tied to a particular enclosing object
Ordinary local classA blockWhen declared in an instance contextA named implementation needed within that block
Anonymous classAn expressionWhen declared in an instance contextA short implementation or subclass at the point of use

Local and anonymous classes declared in a static method are still inner classes under JLS terminology, but they have no immediately enclosing instance. Being an inner class and having an enclosing instance aren't interchangeable concepts.

Access Modifiers and Encapsulation

Member classes declared in a class can be public, protected, private, or package-private. Top-level classes cannot be private or protected. A private member class is useful for hiding an implementation type without exposing another top-level type.

Nesting does not mean only the enclosing class can use the type: a public nested type can be part of your API. Code within the same enclosing top-level class can access private members of its nested classes and vice versa, subject to having an object when instance access requires one. See the JLS access-control rules and our guide to Java access modifiers.

Static Nested Classes

A static nested class has no implicit enclosing object. It can access enclosing static members directly and enclosing instance members through an explicit object reference—even private members. The official nested-class tutorial demonstrates this distinction.

public class MainClass {
    private static int mainCount = 4;
    private int pubCount = 2;

    public static class NestedClass {
        static int classCount = 4;
        int instanceCount = 5;

        public void printCount(MainClass owner) {
            System.out.println(mainCount + owner.pubCount);
        }
    }

    public static void main(String[] args) {
        MainClass.NestedClass nested = new MainClass.NestedClass();
        nested.printCount(new MainClass());
    }
}

This example prints 6. The owner parameter supplies the object needed to read pubCount; an unqualified pubCount reference inside printCount would not compile. The nested class can have both class-level state, such as classCount, and per-object state, such as instanceCount.

When to Use a Static Nested Class

Make this your default for a member helper class. It expresses the relationship between types without making every helper object implicitly depend on an enclosing object.

  • Use a private static helper when its name and implementation belong inside one enclosing type.
  • Consider a static nested builder when construction logic belongs with the type being built but doesn't need an already-created outer object.
  • Choose a top-level helper instead when the type has an independent role or is shared across unrelated classes.

Nesting also organizes public APIs. Map.Entry<K, V> is a familiar example of a related nested type, but it is an interface, not an inner class. Nested interfaces are implicitly static.

Non-Static Member Inner Classes

A member inner-class object is associated with a particular enclosing object. Its instance methods can use that enclosing object's fields and methods without receiving the object as a parameter.

public class MainClass {
    private static int mainCount = 4;
    private int pubCount = 2;

    public class NestedClass {
        int instanceCount = 5;

        public void printCount() {
            System.out.println(mainCount + pubCount);
        }
    }

    public static void main(String[] args) {
        MainClass mC = new MainClass();
        MainClass.NestedClass nested = mC.new NestedClass();
        nested.printCount();
    }
}

This also prints 6. Compare new MainClass.NestedClass() in the static example with mC.new NestedClass() here: the second expression selects the enclosing object.

When to Use an Inner Class

Choose a member inner class when its behavior genuinely belongs to a specific enclosing object—for example, a cursor that operates on that object's mutable state. Merely needing access to a private field isn't enough: a static nested class can access it through an explicit reference.

Enclosing Instances and Object Retention

The enclosing-instance relationship has a lifecycle consequence: an inner object that uses enclosing state can keep its enclosing object reachable. If a long-lived listener registry stores that inner object, it may retain the enclosing object's state longer than intended.

Possible retention chain:
long-lived registry
  -> inner listener
       -> enclosing object
            -> other reachable state

Don't assume every compiler emits the same hidden fields; OpenJDK engineers have discussed omitting unused enclosing-instance references. Design around the references your callback needs: unregister listeners when their owner is finished, and prefer explicit dependencies where practical. Our guide to garbage collection in Java explains the reachability model behind this risk.

Static Members in Inner Classes: The Java 16 Change

This complete example requires Java 16 or later:

public class MainClass {
    class NestedClass {
        static int count = 4;

        static int twice(int value) {
            return value * 2;
        }
    }

    public static void main(String[] args) {
        System.out.println(NestedClass.twice(NestedClass.count));
    }
}

It prints 8 without constructing a NestedClass object. Declaring static members doesn't make the class itself static. A static method still has no enclosing object available through MainClass.this.

The older exception was narrower than “anything static final”: a constant variable is a final primitive or String variable initialized with a constant expression. For example, static final Integer LIMIT = 10; is not a constant-variable declaration under the JLS definition.

Local Classes and Variable Capture

A local class gives you a named implementation within a block. Its declaration cannot have public, protected, or private access modifiers, as specified in JLS §14.3.

public class MainClass {
    private static int mainCount = 4;
    private int pubCount = 2;

    public void addCounts() {
        int adjustment = 0;

        class LocalClass {
            void addMembers() {
                System.out.println(mainCount + pubCount + adjustment);
            }
        }

        new LocalClass().addMembers();
    }

    public static void main(String[] args) {
        new MainClass().addCounts();
    }
}

This prints 6. Because addCounts is an instance method, the local class can use pubCount through the enclosing object. In a static method, there would be no implicit enclosing MainClass object.

Final and Effectively Final Variables

The local class captures adjustment: its method uses a variable declared outside the class. Such captured local variables must be final or effectively final, meaning they satisfy the final-variable rules without requiring the final keyword. The same restriction applies to anonymous classes and lambdas; see the JLS rules for captured variable references.

This deliberately invalid method fragment shows the mistake:

// Fragment for MainClass: intentionally does not compile.
static void brokenCapture() {
    int adjustment = 0;
    class LocalClass {
        int value() {
            return adjustment;
        }
    }
    adjustment++; // Makes adjustment no longer effectively final.
    System.out.println(new LocalClass().value());
}

Remove the increment or capture a separate final value. This restriction applies to captured local variables, not ordinary mutable fields such as pubCount. Also, a final object reference does not make the referenced object immutable.

When to Use a Local Class

Use one when you need a name, constructor, multiple instances, or several methods, but only within one block. If the implementation needs reuse beyond that scope, promote it to a member or top-level class.

Anonymous Classes

An anonymous class expression declares a class and creates an instance at the point of use. It can implement an interface or extend a class; it cannot declare an explicit constructor. These rules are defined in JLS §15.9.5.

public class MainClass {
    interface Addition {
        void add();
    }

    private static int mainCount = 4;
    private int pubCount = 2;

    private final Addition addition = new Addition() {
        @Override
        public void add() {
            System.out.println(mainCount + pubCount);
        }
    };

    public static void main(String[] args) {
        MainClass mc = new MainClass();
        mc.addition.add();
    }
}

This prints 6. The nested Addition interface is implicitly static, while the anonymous implementation is an inner class. Naming the interface Addition also avoids shadowing java.lang.Math.

When to Use an Anonymous Class

Use an anonymous class for a short implementation that needs its own fields, several overridden methods, or a superclass. “Anonymous” means the class has no declared name—not that the expression can execute only once.

Anonymous Classes Versus Lambdas

Prefer a lambda for a simple functional interface: an interface whose abstract-method contract can be implemented by one function. A lambda isn't an anonymous class, and this keeps its meaning from the surrounding context rather than referring to a new anonymous object.

public class MainClass {
    @FunctionalInterface
    interface Label {
        String value();
    }

    private final String name = "outer";

    void compare() {
        Label anonymous = new Label() {
            private final String name = "anonymous";

            @Override
            public String value() {
                return this.name;
            }
        };
        Label lambda = () -> this.name;

        System.out.println(anonymous.value());
        System.out.println(lambda.value());
    }

    public static void main(String[] args) {
        new MainClass().compare();
    }
}

The output is anonymous, then outer. In the anonymous implementation, this.name reads its own field. In the lambda, it reads the enclosing MainClass field, following JLS §15.27.2. See our guide to Java lambda expressions for more examples.

Shadowing and EnclosingClass.this

Shadowing means a declaration prevents a same-named declaration in an enclosing scope from being referenced by its simple name. It isn't unique to nested-class fields: parameters and local variables can shadow fields too. Qualification identifies the object whose field you want.

public class MainClass {
    int myInt = 1;

    public class NestedClass {
        int myInt = 5;

        public void print() {
            System.out.println(myInt);
            System.out.println(this.myInt);
            System.out.println(MainClass.this.myInt);
        }
    }

    public static void main(String[] args) {
        MainClass mc = new MainClass();
        MainClass.NestedClass nc = mc.new NestedClass();
        nc.print();
    }
}

This prints 5, 5, and 1. Here, this is the nested object, while MainClass.this selects its enclosing object. That qualified enclosing-instance expression isn't available in a static nested class. The JLS shadowing rules explain the underlying name lookup.

Common Mistakes

  • Using non-static nesting by habit. Start with static for member helpers; add an enclosing-instance dependency only when the design needs it.
  • Confusing access permission with object availability. Nesting can permit private access, but instance members still belong to an object.
  • Applying the pre-Java-16 static-member restriction to modern source levels. Check the project's configured source level, not just the installed JDK.
  • Turning every helper into a nested class. Keep independent types independent, and avoid deeply nested implementations that make navigation harder.

Choosing the Right Form

Choose scope first, then ownership. Use a static member class for a related named type, a member inner class for deliberate enclosing-object behavior, a local class for a block-scoped named implementation, and an anonymous class for a short implementation needing class features. For a simple functional-interface implementation, reach for a lambda.

The organizing principle hasn't changed: keep closely associated types together. The important compatibility correction is that inner classes can declare static members at Java 16 and later source levels.

FAQ

What is the difference between a nested class and an inner class in Java?

A nested class is declared inside another class or interface. An inner class is a nested class that is neither explicitly nor implicitly static; ordinary local classes and anonymous classes are inner classes too.

How do you instantiate a non-static inner class?

For a non-static member inner class, first obtain an enclosing object, then use MainClass.NestedClass nested = mC.new NestedClass();. A static nested class instead uses new MainClass.NestedClass() and needs no enclosing object.

Can a static nested class access instance fields of its outer class?

Yes, through an explicit outer-object reference, including private fields accessible within the same enclosing top-level class. It cannot access them through an implicit enclosing instance because it has none.

Can inner classes declare static methods in Java 16 and later?

Yes. Since Java 16, inner classes may declare static methods, fields, member types, and static initializers. Earlier source levels allowed only static members that were constant variables and prohibited static initializers.

When should I use a lambda instead of an anonymous class?

Use a lambda for a simple functional-interface implementation that doesn't need its own fields or additional methods. Use an anonymous class when you need class-specific features, such as subclassing or overriding several methods; remember that this has different meanings in the two forms.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me