ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java 内部类系列①:别再分不清 “静态内部类“ 和 “成员内部类“!this$0、内存泄漏、JDK16 一次讲透

Java 内部类系列①:别再分不清 “静态内部类“ 和 “成员内部类“!this$0、内存泄漏、JDK16 一次讲透 文章收录专栏Java 核心原理全解源码・并发・面试实战老读者都知道我写技术文章有个习惯不背结论看字节码看规范。内部类这块网上 90% 的文章都停留在「静态内部类要加 static」的层面但面试官真正想听的是术语边界清不清楚、this$0 为什么会造成内存泄漏、JDK16 到底改了什么。这是《Java 内部类系列》的第一篇先立术语坐标系再把内部类家族里最常用的两类 ——静态内部类和成员内部类讲透。四类内部类在内存引用、生命周期、访问权限、实例化规则、生产风险上差异极大这一篇你掌握前两类后面两篇局部 / 匿名、Lambda就顺了。一、先把官方术语说清楚面试第一句的加分项JLS 的定义非常严格很多老手都会栽在这嵌套类Nested Class所有定义在另一个类内部的类统称。内部类Inner Class仅指非静态嵌套类 —— 成员内部类、局部内部类、匿名内部类。静态嵌套类Static Nested Class带 static 修饰的嵌套类官方不认为它是内部类是独立嵌套类型。国内习惯把静态嵌套类叫「静态内部类」无妨但你要心里清楚从语言规范上讲它是独立类型。面试第一句话把这个边界讲清楚立刻和背答案的拉开差距。class Outer { static class StaticInner {} // 静态嵌套类俗称静态内部类无 this$0 class MemberInner {} // 成员内部类真·内部类 void method() { class LocalInner {} // 局部内部类系列② Runnable r () - {}; // Lambda系列③ } }二、静态内部类最安全的内部类被 static 修饰的嵌套类属于外部类的静态成员与外部类实例完全解耦。核心特性不持有外部类实例引用没有 this$0 字段可独立实例化、独立存活。外部类加载时不会加载、初始化静态内部类只有静态内部类被主动使用时才触发类加载按需加载。生命周期与外部类实例无关外部类被 GC 回收不影响静态内部类。编译生成独立字节码文件Outer$StaticInner.class。访问权限可访问外部类全部静态成员含 private 静态字段、静态方法。私有互访机制有版本差异JDK8–10 靠编译器生成access$xxx合成访问方法JDK11 引入 nestmates 后大多数场景不再生成底层机制系列④细讲。无法直接访问外部类实例成员只能手动持有外部类引用间接操作。支持 public/protected/ 默认 /private 修饰私有静态内部类仅外部类可访问。实例化方式—— 无需外部类实例直接通过类名创建Outer.StaticInner inner new Outer.StaticInner();可包含内容语法层面非常接近顶级类 —— 可定义静态 / 非静态字段、方法、构造器、静态 / 实例初始化块。注意它依旧是嵌套类不是真正顶层类不能拥有包声明等顶层类能力。优缺点与典型场景优点无隐式内存泄漏、按需初始化、高解耦、便于测试。缺点无法直接操作外部实例状态若含可变字段需自行保证线程安全。典型场景建造者模式、HashMap.Node、Map.Entry、专属工具类、常量封装。Builder 模式就是教科书级应用public class User { private final String name; private final int age; private User(Builder b) { this.name b.name; this.age b.age; } public static class Builder { // 静态内部类 private String name; private int age; public Builder name(String n) { this.name n; return this; } public Builder age(int a) { this.age a; return this; } public User build() { return new User(this); } } } // User u new User.Builder().name(木雨).age(18).build();三、成员内部类this$0 与内存泄漏的根源无 static 修饰定义在类的成员位置语义上强依赖外部类实例。核心特性JDK18 编译目标下成员内部类只要实例化就必定合成一个 final 字段 this$0持有创建它的外部类实例强引用与创建时处于静态还是实例上下文无关。必须依托外部类实例创建不能独立存在。可通过Outer.this区分与外部类同名的成员。编译字节码Outer$MemberInner.class。this$0 是编译器合成字段业务代码禁止修改JDK16 起强封装默认启用、JDK17 全面生效后反射访问合成字段也受到限制改它属于依赖编译器内部实现生产严禁。访问权限可访问外部类所有成员私有 / 公有、静态 / 实例。实例化与反射Outer.Inner inner new Outer().new Inner(); // 必须先有外部实例静态方法中也可以通过外部类对象引用创建成员内部类。反射重点构造器隐式携带第一个外部实例参数源码不可见反射实例化必须传入外部实例私有内部类需要setAccessible(true)。JDK16 语言规范更新注意不是 JEP-397这是最容易答错的点很多人背成 JEP-397其实是错的JEP-397 是 Sealed ClassesJDK17。JDK16 之前非静态内部类不能拥有静态成员仅允许 static final 编译期常量禁止普通静态字段、静态方法、静态初始化块。JDK16 起放开限制允许静态字段、静态方法、静态初始化块。该变更随JEP 395Records配套的 JLS 更新《Local and Nested Static Declarations》落地对应 CSRJDK-8254321但所有静态成员不能访问外部实例、局部变量、类型参数。静态常量赋值编译期常量表达式在所有版本均不受限。class Outer { class Inner { static int MAX 10; // JDK16 合法JDK16 之前编译报错 static void hello() {} // JDK16 合法 // static void bad() { System.out.println(Outer.this); } // 编译错误 } }main 方法细节JDK16 语法上可以在非静态内部类里声明public static void main但生产不推荐把非静态内部类作为程序入口主类优先用顶层类或静态嵌套类。静态内部类可以正常定义并运行 main。内存泄漏风险内部类实例被长生命周期对象静态集合、线程池、单例持有会导致外部类对象无法被 GC—— 经典内存泄漏场景class Outer { class Task implements Runnable { Override public void run() { /* 用不到任何外部状态 */ } } void submit() { executor.submit(new Task()); // Task 被线程池长期持有 → Outer 无法被 GC } }上面这段即使 Task 完全没用到外部状态JDK18 之前也会强制持有 Outer。这就是为什么老代码里动不动就内存泄漏。使用场景迭代器实现、事件回调、与外部实例强耦合且生命周期可控的逻辑。四、一张表对比静态 vs 成员维度静态内部类成员内部类依赖外部实例不依赖必须依赖this$0无必定生成JDK18 不引用外部状态时可省略属编译器优化访问外部成员仅静态成员全部成员私有 / 公有、静态 / 实例实例化new Outer.StaticInner()new Outer().new Inner()内存泄漏风险无高风险独立存活可以不可以五、小结与下篇预告静态 vs 成员本质差异就一条成员内部类持有外部实例引用this$0静态内部类不持有。这一条能推出访问权限、实例化方式、内存泄漏风险的所有结论。下一篇我们讲另外两个更灵活的成员局部内部类作用域与 effectively-final 底层原理和匿名内部类合成构造器与双括号初始化陷阱记得关注「木雨」不迷路。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进