# Inner classes

Source: https://codewiki.com/java/inner-classes/

> - **what**: A nested class is declared inside another class or interface; only a nested class that isn't explicitly or implicitly `static` is an inner class.
> - **trap**: A non-static member inner class belongs to an outer object; making an otherwise static helper an inner class changes the object graph and its lifetime.
> - **fix**: Use a member inner class only when it genuinely needs implicit access to an outer instance; otherwise prefer a `static` member class, and check local callbacks for capture and `this` semantics.

## What it is and why it exists

Java lets you put a class declaration inside another class, an interface, or a block. You can also declare a nameless class directly in an object creation expression. Nesting keeps an implementation that serves one host close to where it is used while retaining fields, methods, inheritance, and interface implementation.

In the specification, “nested class” and “inner class” aren't synonyms. Nested classes include member, local, and anonymous classes; inner classes are the ones among them that aren't explicitly or implicitly `static`. The common phrase “static inner class” is therefore inaccurate. The correct term is “static nested class.”

A member inner class suits behavior that can't exist apart from one outer object, such as an iterator that directly reads its collection's current state. A static nested class suits a type that belongs to its host only for naming and visibility, such as a builder, result type, or private implementation. Local and anonymous classes keep a small implementation near one method or expression.

Nesting isn't inheritance, and it doesn't create an automatic encapsulation boundary. An outer class and its nested classes can access each other's `private` members under Java's access rules, but a public nested type can still become external API. Choose a form after deciding the type's owner, visibility, and instance lifetime.

When an implementation needs only one functional-interface method, a lambda is often shorter than an anonymous class. A lambda isn't an anonymous class, though: their `this` meanings, permitted members, and target types differ. You can't mechanically exchange them when those differences affect behavior.

## How it works

The compiler determines the category from where a declaration appears and whether it is `static`, then resolves names from lexical scope. The important questions aren't how many braces surround the code, but whether an object needs an enclosing instance and whether it captures local names.

The classification below uses Java 25 specification terms. A member class is a member of its outer type. Local and anonymous classes aren't members of any package, class, or interface, even though their declarations appear in a method's or class's lexical surroundings.

### Four common forms

| Form | Named | Inner class | Creation or use |
| --- | --- | --- | --- |
| Non-static member class | Yes | Yes | `outer.new Member()` |
| Static member class | Yes | No | `new Outer.Nested()` |
| Local normal class | Yes, within its block | Yes | Use its simple name within the declaration scope |
| Anonymous class | No simple name | Yes | `new Supertype(...) { ... }` |

Member enum classes, member record classes, and member classes of interfaces are implicitly `static`, so they are nested classes but not inner classes. Local enum and local record classes are implicitly `static` too. Local normal and anonymous classes remain inner classes even when declared in a static context.

An anonymous class is declared implicitly by a class instance creation expression or an enum constant with a class body. It selects one direct superclass or direct superinterface through that expression and can't explicitly declare a constructor. It can use field initializers or an instance initializer to initialize its own fields.

### Enclosing instances and name resolution

An object of a non-static member inner class has an enclosing instance. Outside the outer class, `outer.new Member()` names that object explicitly. A static nested class has no such implicit object relationship and can be created through the outer type name.

Inside an inner-class method, `this` denotes the inner object. Use `Outer.this` to name the lexically enclosing object; the qualifier also resolves ambiguity when fields share a name. A static context has no enclosing `this`, so a local or anonymous class declared there can still be an inner class by the specification's classification while having no immediately enclosing instance.

“A static nested class can access only static members of the outer class” is misleading shorthand. More precisely, it has no implicit outer object, so it can't use outer instance members unqualified. If it has an explicit outer object reference, it can access that object's members, including `private` members permitted by the language's access rules.

A member inner class may declare `static` fields, methods, member types, and static initializers. Java 16 relaxed the old restriction. Static members have neither an inner-class object's `this` nor an enclosing instance to use.

### Capturing local variables

A local or anonymous class may refer to local variables, parameters, and exception parameters in its lexical scope, but those variables must be `final` or effectively final. An effectively final variable omits the modifier but satisfies the assignment rules such that adding `final` wouldn't introduce a compilation error.

The rule restricts reassignment of the variable, not mutation of an object. A captured `List` variable can't later point to another list, but the inner class can still call `add()` on the original list. Using a one-element array to evade the reassignment rule may compile, but it often makes state ownership harder to see.

Capture happens on demand. A local class doesn't retain every local name merely because it is declared in the same method. Conversely, one captured name can refer to a large object graph, so the number of names says little about how much data stays reachable.

The compiler must preserve the values that the inner object is required to observe after the original method returns. The fields, constructor parameters, and class files it generates to do so are implementation details. Application code must not depend on their synthetic names.

### Access and type ownership

A member class may be `public`, `protected`, package-private, or `private`. A local class can't use those access modifiers, and its name is visible only in the corresponding block's scope. An anonymous class has no simple name for another declaration to reference at all.

Nested classes can access private host members, and the host can access private nested members. That convenience shouldn't become indiscriminate state sharing. If a helper type needs only two values at construction, explicit parameters are usually clearer than implicit access to the entire outer object.

A non-static member inner class of a generic outer class is in scope of the outer type parameters. A static nested class can't directly use the outer class's type parameters; it declares its own when needed. This difference often reveals whether a type truly depends on an outer instance.

## Examples

These three examples cover member forms, local capture, and the difference between `this` in an anonymous class and a lambda. Every output shown was produced locally by compiling with OpenJDK 21.0.12 and `javac --release 21 -Xlint:all`; the semantics remain valid in Java 25.

### Member inner and static nested classes

The picker label inherently belongs to one warehouse object, so `Picker` reads `NestedKinds.this.warehouse`. `Snapshot` receives its own data and doesn't depend on a `NestedKinds` instance.

<!-- quick -->

```java
// file: NestedKinds.java
public class NestedKinds {
    private final String warehouse;

    NestedKinds(String warehouse) {
        this.warehouse = warehouse;
    }

    final class Picker {
        String label(int orderId) {
            return NestedKinds.this.warehouse + "/order-" + orderId;
        }
    }

    static final class Snapshot {
        private final String warehouse;

        Snapshot(String warehouse) {
            this.warehouse = warehouse;
        }

        String label() {
            return warehouse + "/snapshot";
        }
    }

    public static void main(String[] args) {
        var north = new NestedKinds("north");
        var picker = north.new Picker();
        var snapshot = new NestedKinds.Snapshot("south");

        System.out.println(picker.label(17));
        System.out.println(snapshot.label());
    }
}
```

```text
north/order-17
south/snapshot
```

<!-- /quick -->

Creating `picker` must supply `north`, so their lifetimes have an object-level connection. The `snapshot` constructor contains only explicit parameters. Putting it in `NestedKinds` expresses naming and visibility, not instance ownership.

If `Snapshot` received a `NestedKinds` parameter, it could still read that object's private field. `static` removes the implicit enclosing instance, not private access within the same nest.

### Share captured state between local and anonymous classes

`Recorder` captures `tenant` and `events`, and the returned anonymous `Runnable` captures `recorder`. Two `audit()` calls create two state groups, while repeated calls to one result keep modifying its original list.

```java
// file: LocalAndAnonymous.java
import java.util.ArrayList;
import java.util.List;

public class LocalAndAnonymous {
    static Runnable audit(String tenant) {
        List<String> events = new ArrayList<>();

        class Recorder {
            void add(String action) {
                events.add(tenant + ":" + action);
            }

            String summary() {
                return String.join(" | ", events);
            }
        }

        Recorder recorder = new Recorder();
        recorder.add("created");

        return new Runnable() {
            @Override
            public void run() {
                recorder.add("sent");
                System.out.println(recorder.summary());
            }
        };
    }

    public static void main(String[] args) {
        Runnable north = audit("north");
        Runnable south = audit("south");
        north.run();
        south.run();
        north.run();
    }
}
```

```text
north:created | north:sent
south:created | south:sent
north:created | north:sent | north:sent
```


Neither `events` nor `recorder` is reassigned, so both satisfy the effectively final rule; `events.add()` changes the object instead. Uncommenting an `events = new ArrayList<>()` assignment would make the name not effectively final even if the assignment followed the inner-class creation, and the compiler would reject the capture.

The example has no `LocalAndAnonymous` instance because `audit()` is static. Capturing a local variable and retaining an outer-class object are two separate relationships. Trace them separately during review.

### Compare `this` in an anonymous class and a lambda

An anonymous class creates a new object scope, so its `this.owner` is its own field. A lambda doesn't introduce a new `this`; its `this.owner` still denotes the `ThisBinding` object.

```java
// file: ThisBinding.java
public class ThisBinding {
    private final String owner = "enclosing";

    void compare() {
        Runnable anonymous = new Runnable() {
            private final String owner = "anonymous";

            @Override
            public void run() {
                System.out.println("anonymous this: " + this.owner);
                System.out.println("outer this: " + ThisBinding.this.owner);
            }
        };

        Runnable lambda =
                () -> System.out.println("lambda this: " + this.owner);

        anonymous.run();
        lambda.run();
    }

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

```text
anonymous this: anonymous
outer this: enclosing
lambda this: enclosing
```

An anonymous class can also declare fields, extra methods, and instance initializers, and it can extend an ordinary class. A lambda can target only a functional interface. If an anonymous class relies on its own `this` or extra state, redesign those semantics before replacing it with a lambda.

### Declare static members in an inner class

`Formatter` is a non-static member inner class, yet it legally declares a static field and method. Its two inner objects share `created`, while the instance method `label()` still reads `prefix` through an enclosing instance.

```java
// file: InnerStatics.java
public class InnerStatics {
    private final String prefix;

    InnerStatics(String prefix) {
        this.prefix = prefix;
    }

    class Formatter {
        private static int created;

        Formatter() {
            created++;
        }

        static int created() {
            return created;
        }

        String label(int id) {
            return prefix + id;
        }
    }

    public static void main(String[] args) {
        var owner = new InnerStatics("A-");
        var first = owner.new Formatter();
        owner.new Formatter();

        System.out.println(InnerStatics.Formatter.created());
        System.out.println(first.label(7));
    }
}
```

```text
2
A-7
```

The static method is called through `InnerStatics.Formatter.created()` and needs neither a `Formatter` nor an `InnerStatics` object. It can't read `prefix` directly. The instance method can because it has both an inner object and its enclosing instance.

This example compiles locally with `--release 21` and doesn't rely on syntax added in Java 25. The same declaration fails under the older rules if a project still targets Java 15 or earlier source.

## Pitfalls

### Calling every nested form an inner class

> **Pitfall:** Treating a static nested class as one of “four inner classes” makes later reasoning about enclosing instances and `this` contradictory. Implicit `static` on records, enums, and interface members adds further confusion.

**Fix:** Use “nested class” for the whole category, then say whether the class is inner. Name the member, local, or anonymous form and any explicit or implicit `static`; don't infer the source category from a `$` in a file name.

### Retaining an outer instance for an unrelated helper

> **Pitfall:** An instance of a non-static member inner class is associated with an outer instance. If the inner object enters a cache, listener registry, executor queue, or another long-lived container, the outer object and its graph may remain reachable too.

**Fix:** Make a helper a static nested class when it doesn't need implicit instance members, and pass only the data it genuinely needs to its constructor. A callback that does need outer state should have an explicit unregister or cancellation path, with a lifecycle test for the release boundary.

### Evading effectively final with a mutable holder

> **Pitfall:** Generated code often changes a local counter into `int[] count = {0}` or an `AtomicInteger` solely so an anonymous class can modify it. The array hides shared mutable state, while one atomic operation doesn't make a whole business transaction atomic.

**Fix:** Decide which object and concurrency boundary own the state first. Put state with several operations or invariants in a named class. Use an atomic class only when the contract truly needs a cross-thread atomic counter, and test the complete compound operation.

### Mechanically replacing an anonymous class with a lambda

> **Pitfall:** A lambda works only for a functional interface and doesn't establish a new `this`. An anonymous class can extend a class and declare fields or extra methods, so shorter syntax doesn't prove equivalent behavior.

**Fix:** Check the target type, overload selection, `this` and `Outer.this`, object identity, and whether extra members affect behavior. Convert only after confirming that one functional-interface operation is enough, then run tests that cover callback registration and invocation timing.

### Depending on compiler-generated binary names

> **Pitfall:** `Outer$1`, `Outer$1Local`, and `this$0` are common `javac` artifacts, not stable source contracts for application code. Reordering or adding an anonymous class can break reflection, configuration, or serialization logic based on those names.

**Fix:** Give a type a proper name and public contract when it needs a stable identity. Use `javap` to inspect a particular build during diagnosis; production code should find behavior through interfaces, explicit registration, and controlled serialization formats.

### Applying pre-Java 16 static-member rules

> **Pitfall:** Older material says an inner class may declare only static constants, not ordinary static members or static initializers. Java 16 relaxed that restriction, so applying it to Java 25 code produces bogus fixes.

**Fix:** Check the language rules for the project's `--release` target. Static members are now legal, but their code is a static context and can't use an inner object or enclosing instance through `this`.

<!-- deep -->

## Class files and nestmates

Source nesting doesn't put every implementation into one class file. Compiling the example with local `javac` produces `NestedKinds.class`, `NestedKinds$Picker.class`, and `NestedKinds$Snapshot.class`; local and anonymous classes also receive class files. The `$` form is a binary naming result from this compiler, not source API.

Running `javap -p` on `NestedKinds$Picker` under local OpenJDK 21.0.12 shows a synthetic field named `this$0` with type `NestedKinds`, and its constructor accepts a `NestedKinds`. `NestedKinds$Snapshot` has no such field. This observation explains the object relationship, but the field name and translation strategy aren't Java language guarantees.

Since Java 11 introduced nestmate access control, class files from the same nest can establish their runtime relationship through `NestHost` and `NestMembers` attributes and perform permitted private access directly. Older compilation targets may use synthetic access methods. It is no longer correct to claim that reading a private outer field in modern Java 25 necessarily generates `access$000`.

`InnerClasses`, `EnclosingMethod`, `NestHost`, and `NestMembers` describe different relationships. When debugging reflection, class loading, or tooling, inspect the actual class files and target version instead of reconstructing the entire lexical structure from one `$` name.

## Edges that change the category

Inner class doesn't mean “always stores an outer object.” A non-static member inner class requires an enclosing instance. A normal local or anonymous class in a static method has no enclosing `this`, even though the specification still classifies it as an inner class. Capturing local variables is another independent decision.

Every nested interface is implicitly `static`; there are no inner interfaces. Member and local enum and record classes are implicitly `static` as well. Changing a local normal class into a local record doesn't merely add concise data-carrier syntax; it also changes its relationship to enclosing instances and outer type parameters.

A static method, static field initializer, or static initializer inside an inner class is a static context. It can use the class's static members but not `Outer.this` or the inner object's `this`. Permission to declare a static member doesn't grant an enclosing object to its code.

An anonymous class is always an inner class, but it doesn't always have an enclosing instance. Its direct supertype comes from the creation expression, and the compiler declares its constructor implicitly. When initialization needs several construction paths or extra methods visible to callers, a named class usually expresses the contract better.

## Lifetime and API boundaries

An implicit enclosing instance is a strong-reference path in the object graph. While the inner object remains reachable, the outer object may remain reachable through that path. Whether this becomes a real leak depends on how long roots such as registries, threads, and caches keep it alive, not on the word `inner` alone.

Captured local objects follow the same reachability rule. A callback that reads only `request.userId()` but captures the entire `request` keeps request-associated data reachable. Extracting the required immutable identifier before capture shrinks the graph, but the callback still needs cancellation and unregistration where appropriate.

Anonymous and local classes work well for narrow implementations. Once an implementation has several operations, complex state, independent testing needs, or a stable serialization format, a named member or top-level class is usually clearer. Nesting should express ownership, not merely reduce the number of files.

Test public behavior and lifetime rather than synthetic fields. Create two outer instances and interleave calls through their inner objects. For captured state, invoke callbacks repeatedly after registration and add a concurrency test. Use heap dumps, reference paths, and `javap` only when you need to investigate the concrete retention mechanism.

<!-- /deep -->

[Checkpoint: java/inner-classes](https://codewiki.com/java/inner-classes/#checkpoint)

## Further reading

- [Java Language Specification 25: inner classes and enclosing instances](https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html#jls-8.1.3)
- [Java Language Specification 25: local class and interface declarations](https://docs.oracle.com/javase/specs/jls/se25/html/jls-14.html#jls-14.3)
- [Java Language Specification 25: anonymous class declarations](https://docs.oracle.com/javase/specs/jls/se25/html/jls-15.html#jls-15.9.5)
- [Java Language Specification 25: simple expression names and capture restrictions](https://docs.oracle.com/javase/specs/jls/se25/html/jls-6.html#jls-6.5.6.1)
- [Java Virtual Machine Specification 25: the `NestHost` attribute](https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-4.html#jvms-4.7.28)
