# 内部类

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

> - **what**: 嵌套类（nested class）是在另一个类或接口中声明的类；其中没有显式或隐式使用 `static` 的才是内部类（inner class）。
> - **trap**: 非静态成员内部类依附于一个外部对象；把本可静态的辅助类写成内部类，会让对象关系和生命周期发生变化。
> - **fix**: 只有确实需要隐式访问外部实例时才使用成员内部类；其余成员类优先声明为 `static`，局部回调还要检查捕获和 `this` 的含义。

## 是什么，为什么存在

Java 允许把类声明放进另一个类、接口或代码块，也允许在对象创建表达式中直接声明一个没有名称的类。嵌套把只服务于一个宿主的实现放在使用位置附近，同时仍保留类的字段、方法、继承与接口实现能力。

规范中的「嵌套类」与「内部类」不是同义词。嵌套类包含成员类、局部类和匿名类；内部类只包含其中没有显式或隐式 `static` 的类。因此，「静态内部类」这个常见说法并不准确，应该叫「静态嵌套类」。

成员内部类适合表示离不开某个外部对象的行为，例如迭代器需要直接读取所属集合的当前位置。静态嵌套类适合只在命名与可见性上属于宿主的类型，例如构建器、结果类型或私有实现。局部类和匿名类则把很小的实现限制在一个方法或表达式附近。

嵌套不等于继承，也不会自动产生封装边界。外部类与其嵌套类可以按 Java 访问规则互相访问 `private` 成员，但公开嵌套类型仍可能成为外部 API。选择形式时要先确定类型的所有者、可见性和实例生命周期。

当实现只需要一个函数式接口的方法时，lambda 往往比匿名类短。不过 lambda 不是匿名类：两者的 `this`、可声明成员和适用目标不同。若这些差异会影响行为，就不能把两种写法机械互换。

## 工作原理

编译器根据声明出现的位置和 `static` 状态决定类别，再根据词法作用域解析名称。关键不是大括号嵌套了几层，而是对象是否需要外围实例，以及局部名称是否被捕获。

下面的分类采用 Java 25 规范术语。成员类是外部类型的成员；局部类和匿名类不是任何包、类或接口的成员，即使它们写在方法或类体的词法范围内。

### 四种常见形式

| 形式 | 有名称 | 是否为内部类 | 创建或使用方式 |
| --- | --- | --- | --- |
| 非静态成员类 | 有 | 是 | `outer.new Member()` |
| 静态成员类 | 有 | 否 | `new Outer.Nested()` |
| 局部普通类 | 有，仅在代码块内 | 是 | 在声明作用域内使用简单名称 |
| 匿名类 | 无简单名称 | 是 | `new Supertype(...) { ... }` |

成员枚举类、成员记录类以及接口的成员类都隐式为 `static`，所以它们是嵌套类而不是内部类。局部枚举类与局部记录类也隐式为 `static`。普通局部类和匿名类仍属于内部类，即使声明出现在静态上下文中。

匿名类通过类实例创建表达式或带类体的枚举常量隐式声明。它只能在该表达式中选择一个直接父类或直接父接口，不能显式声明构造器；需要初始化自身字段时可以使用字段初始化器或实例初始化块。

### 外围实例与名称解析

非静态成员内部类的对象带有一个外围实例（enclosing instance）。在外部类之外创建它时，语法中的 `outer.new Member()` 明确指定这个对象。静态嵌套类没有这条隐式对象关系，可以直接通过外部类型名创建。

内部类方法中的 `this` 指向内部类对象。需要指明词法外围对象时使用 `Outer.this`；字段同名时，这个限定也能消除歧义。静态上下文没有外围 `this`，所以在那里声明的局部类或匿名类虽然仍按规范归入内部类，却可能没有直接外围实例。

「静态嵌套类只能访问外部类的静态成员」是一种容易误导的简写。准确说法是：它没有隐式外部对象，因此不能直接使用外部实例成员；如果手中有外部对象引用，仍可通过该引用访问其成员，包括按语言规则可访问的 `private` 成员。

成员内部类也可以声明 `static` 字段、方法、成员类型和静态初始化块。这项限制已在 Java 16 放宽；`static` 成员没有内部类对象的 `this`，也没有可供使用的外围实例。

### 局部变量捕获

局部类和匿名类可以引用词法作用域中的局部变量、形参和异常参数，但这些变量必须是 `final` 或实际 final（effectively final）。后者没有写出 `final`，却满足加上 `final` 也不会引入编译错误的赋值约束。

这个规则限制的是变量重新赋值，不是对象状态。捕获的 `List` 变量不能再指向另一份列表，但内部类仍可调用 `add()` 修改原列表。用单元素数组绕过重新赋值限制虽然能编译，却常把状态所有权藏得更深。

捕获按需要发生。局部类没有引用某个局部名称时，不会因为处于同一方法就自动保留它。反过来，一个被捕获的名称可能指向很大的对象图，所以名称数量不能说明实际保留的数据大小。

编译器必须让内部类对象在原方法返回后仍能观察到规范要求的值。具体如何生成字段、构造器参数和类文件属于实现细节；应用代码不应依赖这些合成名称。

### 访问与类型所有权

成员类可以声明为 `public`、`protected`、包级可见或 `private`。局部类不能使用这些访问修饰符，它的名称只在对应代码块的作用域内可见；匿名类甚至没有可供其他声明引用的简单名称。

嵌套类能够访问宿主的私有成员，宿主也能访问嵌套类的私有成员。这种访问便利不应演变成随意共享状态。若一个辅助类型只需要构造时传入的两个值，显式参数通常比隐式读取整个外部对象更清楚。

泛型外部类的非静态成员内部类处于外部类型参数的作用域内。静态嵌套类不能直接使用外部类的类型参数；需要泛型时，它应声明自己的类型参数。这条差异常能暴露某个类型是否真的依赖外部实例。

## 示例

下面三个示例依次展示成员形式、局部捕获，以及匿名类与 lambda 的 `this` 差异。所有输出均由本地 OpenJDK 21.0.12 使用 `javac --release 21 -Xlint:all` 编译后实际运行得到；所用语义在 Java 25 中保持有效。

### 成员内部类与静态嵌套类

`Picker` 的标签天然属于某个仓库对象，所以它读取 `NestedKinds.this.warehouse`。`Snapshot` 自己接收所需数据，不依赖 `NestedKinds` 实例。

<!-- 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 -->

`picker` 的创建表达式必须提供 `north`，因此两者的生命周期存在对象级联系。`snapshot` 的构造只包含显式参数；把它放在 `NestedKinds` 中表达的是命名和可见性，而不是实例所有权。

若 `Snapshot` 收到一个 `NestedKinds` 参数，它仍能读取该对象的私有字段。`static` 取消的是隐式外围实例，不是同一嵌套范围中的私有访问权限。

### 让局部类与匿名类共享捕获状态

`Recorder` 捕获 `tenant` 和 `events`，返回的匿名 `Runnable` 又捕获 `recorder`。两次 `audit()` 调用产生两组状态，而重复调用同一个结果会继续修改原列表。

```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
```


`events` 和 `recorder` 都没有被重新赋值，所以符合实际 final 规则；`events.add()` 修改的是对象内容。若取消注释一条 `events = new ArrayList<>()`，即使赋值写在创建内部类之后，名称也不再是实际 final，编译器会拒绝捕获。

示例没有任何 `LocalAndAnonymous` 实例，因为 `audit()` 是静态方法。这也说明捕获局部变量与持有外部类对象是两条不同关系。代码审查时应分别追踪它们。

### 比较匿名类与 lambda 中的 `this`

匿名类建立新的对象作用域，所以它的 `this.owner` 是自身字段；lambda 不引入新的 `this`，其中的 `this.owner` 仍指向 `ThisBinding` 对象。

```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
```

匿名类（anonymous class）还可以声明字段、额外方法和实例初始化块，并且可以继承普通类。lambda 只能以函数式接口为目标；若匿名类依赖自身 `this` 或额外状态，改成 lambda 前必须重新设计这些语义。

### 在内部类中声明静态成员

`Formatter` 是非静态成员内部类，却合法声明了静态字段和静态方法。两个内部对象共享 `created`，而实例方法 `label()` 仍通过外围实例读取 `prefix`。

```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
```

静态方法通过 `InnerStatics.Formatter.created()` 调用，不需要 `Formatter` 对象或 `InnerStatics` 对象。它也不能直接读取 `prefix`；实例方法拥有内部对象及其外围实例，才可以进行未限定访问。

这个示例在本地使用 `--release 21` 编译通过，没有依赖 Java 25 新增语法。若项目仍以 Java 15 或更早版本为源码目标，同一声明会因旧规则而失败。

## 陷阱

### 把所有嵌套形式都叫内部类

> **陷阱:** 「四种内部类」把静态嵌套类也算成内部类，会让后续关于外围实例和 `this` 的推理互相矛盾。记录类、枚举类与接口成员的隐式 `static` 还会进一步放大混乱。

**修复方法：** 先用「嵌套类」概括全部形式，再区分是否为内部类。明确写出成员、局部或匿名，以及显式或隐式 `static`，不要只凭文件名中的 `$` 推断源码类别。

### 为不相关的辅助类保留外围实例

> **陷阱:** 非静态成员内部类会把自己的实例与某个外部实例关联起来。若内部对象进入缓存、监听器注册表、执行器队列或其他长生命周期容器，外部对象及其对象图也可能保持可达。

**修复方法：** 辅助类不需要隐式实例成员时，把它声明为 `static` 嵌套类，并通过构造器传入真正需要的数据。确实需要外围状态的回调要有明确的注销或取消路径，并用生命周期测试确认释放边界。

### 用可变容器绕过实际 final

> **陷阱:** 生成代码常把局部计数器改成 `int[] count = {0}` 或 `AtomicInteger`，只为让匿名类能够修改它。前者隐藏共享可变状态，后者的单次原子操作也不会自动让整个业务步骤具备原子性。

**修复方法：** 先确定状态属于哪个对象和哪个并发边界。需要多个操作或不变量时，把状态放进命名良好的类；只有契约确实要求跨线程原子计数时才使用原子类，并测试完整的复合操作。

### 把匿名类机械改成 lambda

> **陷阱:** lambda 只适用于函数式接口，而且不会建立新的 `this`。匿名类可以继承类、声明字段和额外方法，因此语法变短并不能证明替换后行为相同。

**修复方法：** 检查目标类型、重载选择、`this` 和 `Outer.this`、对象身份，以及额外成员是否参与行为。确认只需要一个函数式接口操作后再转换，并运行覆盖回调注册与调用时机的测试。

### 依赖编译器生成的二进制名称

> **陷阱:** `Outer$1`、`Outer$1Local` 和 `this$0` 是常见 `javac` 产物，却不是应用代码可以稳定依赖的源码契约。新增或重排匿名类后，基于这些名称的反射、配置或序列化逻辑可能失效。

**修复方法：** 需要稳定标识的类型应拥有正式名称和公开契约。诊断时可以用 `javap` 查看当前构建，生产逻辑则通过接口、显式注册和受控序列化格式定位行为。

### 沿用 Java 16 之前的静态成员规则

> **陷阱:** 旧资料常说内部类只能声明静态常量，不能声明普通静态成员或静态初始化块。这个限制从 Java 16 起已被放宽，对 Java 25 代码继续套用会制造错误修复。

**修复方法：** 按项目的 `--release` 目标核对语言规则。静态成员现在合法，但其代码处于静态上下文，不能借此访问内部类对象或外围实例的 `this`。

<!-- deep -->

## 类文件与 nestmate

源码中的嵌套不会把所有实现塞进一个类文件。用本地 `javac` 编译示例后，会得到 `NestedKinds.class`、`NestedKinds$Picker.class` 和 `NestedKinds$Snapshot.class`；局部类与匿名类也各有对应类文件。`$` 形式是当前编译器的二进制命名结果，不应当作源码 API。

对 `NestedKinds$Picker` 执行 `javap -p`，本地 OpenJDK 21.0.12 显示一个类型为 `NestedKinds` 的合成字段 `this$0`，构造器也接收 `NestedKinds`。`NestedKinds$Snapshot` 没有这个字段。这个观察解释了对象关系，但字段名称和具体翻译策略不属于 Java 语言保证。

Java 11 引入 nestmate 访问控制后，同一嵌套中的类文件可以通过 `NestHost` 与 `NestMembers` 属性建立运行时 nest，并直接进行彼此允许的私有访问。旧编译目标可能使用合成访问方法；对现代 Java 25 代码声称「访问外部私有字段一定生成 `access$000`」已经不正确。

`InnerClasses`、`EnclosingMethod`、`NestHost` 和 `NestMembers` 描述不同关系。需要排查反射、类加载或工具链问题时，应检查实际 class 文件与目标版本，而不是从一个 `$` 名称反推完整的词法结构。

## 改变类别的边界

内部类不等于「一定保存外部对象」。非静态成员内部类需要外围实例；但静态方法中的普通局部类和匿名类没有可用的外围 `this`，即使规范仍把它们归为内部类。是否捕获局部变量又是另一项独立判断。

所有嵌套接口都隐式为 `static`，不存在「内部接口」。成员与局部的枚举类、记录类同样隐式为 `static`。把一个局部普通类改成局部记录类时，不只是获得简洁的数据载体语法，它与外围实例和外部类型参数的关系也会改变。

内部类中的静态方法、静态字段初始化器和静态初始化块属于静态上下文。它们可以使用自身静态成员，却不能使用 `Outer.this` 或内部类实例的 `this`。合法声明静态成员并不意味着静态代码获得了外围对象。

匿名类始终是内部类，但不一定有外围实例。它的直接父类型由创建表达式决定，构造器由编译器隐式声明。若初始化逻辑复杂到需要多个构造路径或外部可见的额外方法，命名类通常更能表达契约。

## 生命周期与 API 边界

隐式外围实例本质上是对象图中的一条强引用路径。只要内部类实例仍可达，外围对象就可能经由这条路径保持可达；是否形成实际泄漏取决于注册表、线程、缓存等根对象能让它存活多久，而不是取决于 `inner` 这个词本身。

捕获的局部对象也遵循同样的可达性原则。一个回调只读取 `request.userId()`，却捕获整个 `request`，会保留请求关联的数据；先提取所需的不可变标识再捕获，可以缩小对象图，但仍应提供取消和注销机制。

匿名类和局部类适合范围很窄的实现。实现开始拥有多个操作、复杂状态、单独测试需求或稳定序列化格式时，命名成员类或顶层类通常更清楚。嵌套位置应表达所有权，不应只是为了减少文件数量。

测试应围绕公开行为和生命周期，而不是合成字段。分别创建两个外部实例，交错调用它们的内部对象；对捕获状态则在注册完成后重复调用并加入并发测试。需要调查保留关系时，再使用堆转储、引用链和 `javap` 验证具体实现。

<!-- /deep -->

[检查点: java/inner-classes](https://codewiki.com/zh/java/inner-classes/#checkpoint)

## 延伸阅读

- [Java 语言规范 25：内部类与外围实例](https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html#jls-8.1.3)
- [Java 语言规范 25：局部类与局部接口声明](https://docs.oracle.com/javase/specs/jls/se25/html/jls-14.html#jls-14.3)
- [Java 语言规范 25：匿名类声明](https://docs.oracle.com/javase/specs/jls/se25/html/jls-15.html#jls-15.9.5)
- [Java 语言规范 25：简单表达式名称与捕获限制](https://docs.oracle.com/javase/specs/jls/se25/html/jls-6.html#jls-6.5.6.1)
- [Java 虚拟机规范 25：`NestHost` 属性](https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-4.html#jvms-4.7.28)
