ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java内部类全解析:四种类型、字节码原理与内存泄漏排查

Java内部类全解析:四种类型、字节码原理与内存泄漏排查 1. 内部类到底是什么先解决为什么存在的问题写 Java 写了五六年的人碰到内部类大多一脸淡定但真要他解释清楚成员内部类为什么不能有 static 方法匿名内部类为什么要求变量 effectively final十有八九会卡壳。这篇文章就是把 Java 内部类这四兄弟从头到尾捋一遍从字节码层面讲到实际开发里的内存泄漏一次性说透。不管你是刚学 Java 基础的新手还是在准备 java 面试题的老兵只要你写过哪怕一丁点 Java 代码这四类内部类就一定会出现在你的项目里。1.1 从字节码说起每个内部类都是一个独立 class 文件Java 内部类Inner Class用一句话概括定义在另一个类内部的类。很多人觉得它只是语法层面的套娃但实际上JVM 根本不认识内部类这个概念。编译之后所有内部类都会生成独立的 .class 文件命名规则是外部类名$内部类名.class。比如Outer里有一个Inner编译后就出现Outer.class、Outer$Inner.class两个文件如果你在一个方法里定义了两个匿名内部类它们会叫Outer$1.class、Outer$2.class。记住这一点后面所有让人惊掉下巴的行为比如为什么内部类能访问外部类的 private 字段为什么匿名内部类不能修改局部变量全都是为了配合这个编译方案产生的。这里要特别强调一个容易忽略的点内部类不是为了让代码看起来整齐而存在的语法糖它本质上是编译期的文法变换。编译器会做三件事给非静态内部类生成一个指向外部类实例的隐藏字段this$0在内部类构造方法里悄悄注入外部类实例参数把内部类对外部类 private 成员的访问改写成编译器生成的桥接方法调用Java 11 之前是access$000这类包级静态方法Java 11 之后由 JVM 原生支持后面会细说。用javap -c Outer$Inner反编译一下你会看到字段列表里赫然躺着一个final Outer this$0。这个this$0就是内部类偷偷保存的外部类引用也是后来所有内存泄漏问题的万恶之源。所以我建议每个学内部类的人都用 javap 亲手反编译一次比背十遍理论都管用。1.2 一张表分清四种内部类很多初学者把内部类当成一种东西其实内部类在 Java 里分成四种每一种的用途、限制、编译结果都不一样。我用一张表把核心差异列出来后面逐类拆解类型定义位置是否持有外部类引用典型应用成员内部类类内部、方法外部持有迭代器、Helper 类、事件处理器局部内部类方法/代码块内部持有方法内临时定义的有名字的类匿名内部类方法/参数位置无类名持有接口实现、回调、事件监听静态内部类类内部带 static不持有Builder、配置项、Map.Entry这张表建议大家直接背下来面试被问内部类有哪几种的时候按这个顺序答再配合一两个例子基本就稳了。判断方法也很简单定义在方法外的就是成员内部类带 static 的就是静态内部类在方法体里并且有名字的是局部内部类既在方法体里又没有名字的就是匿名内部类。1.3 什么时候该用、什么时候别用内部类最大的价值是封装性和可读性。一个辅助类只服务于某个外部类而且和外部类耦合很深把它塞进外部类里面代码的内聚性立马上来了。说白了内部类就是一个不想让外面世界看到、但又得靠外部类活着的类。不过我要泼一盆冷水内部类不是用来炫技的。我 review 过很多代码有人把几十行成员内部类塞在一个大类里结果外部类两千行内部类又一千行整个文件读起来像迷宫。更常见的问题是非静态内部类被外部长期持有引用导致外部类无法被 GC 回收内存涨到怀疑人生。所以我的建议是辅助类强依赖外部类实例且不会被外部类之外的引用持有太久用内部类辅助类逻辑较重、生命周期较长尽量抽成独立的顶层类只是想把几个配置字段归个组优先用静态内部类只是要传一个回调用 lambda 就够了别再去 new 匿名类。2. 成员内部类最基础也最容易踩坑的一种2.1 定义、实例化与编译产物成员内部类就是写在类内部、方法外部的普通 class。看一段最基础的代码public class Outer { private int count 10; class Inner { public void print() { System.out.println(count); // 直接访问外部类私有字段 } } }实例化方式是有讲究的。成员内部类必须先有外部类实例才能外部实例.new 内部类()Outer outer new Outer(); Outer.Inner inner outer.new Inner(); inner.print();注意这个语法outer.new Inner()乍看很怪其实它表达的意思很清楚这个 Inner 属于 outer 这个实例。编译后Inner的构造方法会多一个Outer参数outer.new Inner()这一步就是把这个outer传进去并赋给那个隐藏字段this$0。所以从语义上讲每个成员内部类对象都寄生在一个外部类对象上。也正因为如此成员内部类里可以直接写Outer.this来访问外部类实例这是语法层面的显式引用方式。后面讲 this 指向时还会再碰见。2.2 隐式持有的外部类引用this$0刚才说了成员内部类天生持有外部类引用。它带来的直接后果有三个。第一成员内部类不能定义静态成员。在 Java 16 之前static 方法、static 字段全都不能定义只有 static final 的基本类型常量可以编译器直接报错。原因是成员内部类本身依赖外部类实例它的营养都来自外部对象再搞静态成员会让类加载阶段无所适从。Java 16 之后这个限制被放宽了但绝大多数历史代码和面试题语境里仍然按旧规则来答最稳妥。第二内部类不能被外部类之外的东西随意 new。如果 Inner 是 public 的别处的代码也得先有 Outer 实例再outer.new Inner()这其实是对内部类生命周期必须跟随外部类实例的一种强制。这种强制是好是坏取决于场景但至少语法上把关系焊死了。第三内存泄漏风险。外部类被回收的前提是没有强引用指向它但一个被外部长期持有的内部类会通过this$0强引用外部类外部类就永远回收不掉。最经典的案例就是 Android 里 Handler 泄漏Activity 里的 Handler 写成非静态内部类消息队列里挂着一个延迟 60 秒的消息Message 持有了 HandlerHandler 又持有了 ActivityActivity 即使销毁了也无法回收。这个问题我在第 6 节展开讲。2.3 this 到底指向谁Outer.this 与 this这个点面试经常考因为实际开发里真的会犯。在成员内部类的方法里this指向内部类当前实例Outer.this指向外部类当前实例。写个例子你就明白了public class Outer { private String name outer; class Inner { private String name inner; public void print() { System.out.println(name); // inner System.out.println(this.name); // inner System.out.println(Outer.this.name); // outer } } }如果外部类和内部类有同名成员而你又想访问外部类的必须用Outer.this.成员名。没有这个语法会直接取到内部类的成员。很多人在回调方法里写this.run()结果发现调的是匿名内部类自己的方法一脸懵就是这个原因。2.4 为什么内部类能访问外部类的 private从 access$000 到 nestmate为什么内部类能访问外部类的 private 字段和方法这是面试官特别喜欢往深里挖的一个点。在 Java 11 之前JVM 的访问控制是类级别的一个类编译后只能访问另一个类的 public 和 protected 成员private 就是 private谁也不认。但编译器又不愿意破坏封装性于是它生成了合成桥接方法当内部类要访问外部类的 private 成员时编译器会在外部类里偷偷生成一个包级静态方法比如static int access$000(Outer o) { return o.count; }内部类通过调用这个方法拿到值。这就是access$000的由来。Java 11 引入了 JEP 181也就是基于嵌套的访问控制Nestmate。JVM 现在认识了嵌套类这个概念同一 nest 里的类可以互相直接访问 private 成员不再需要编译器生成那些桥接方法了。所以你在新版 JDK 上反编译基本看不到access$000了内部类直接就能访问外部类私有成员。这个细节的价值在于很多旧博客还在讲access$000你如果能说出Java 11 之后变了面试官对你的好感度会瞬间提升。这不是背出来的而是真正看过字节码、追过 JDK 变更的人才会知道的东西。2.5 实操示例用成员内部类实现一个简易迭代器理论说多了容易飘我给一个实际能跑的代码。Java 集合框架里ArrayList的Itr就是个非静态成员内部类它要访问外部的elementData数组和size所以天然适合做成内部类。我们仿一个极简版public class SimpleListE { private Object[] elements; private int size; public SimpleList(int capacity) { elements new Object[capacity]; } public void add(E e) { elements[size] e; } public IteratorE iterator() { return new Itr(); } // 成员内部类需要访问外部类的 elements 和 size private class Itr implements IteratorE { private int cursor; Override public boolean hasNext() { return cursor size; } SuppressWarnings(unchecked) Override public E next() { return (E) elements[cursor]; } } }注意这里Itr是 private 的成员内部类外部完全不知道它的存在通过iterator()方法返回接口引用给用户。这就是内部类封装性的典型体现实现细节全部藏起来只暴露接口。这个模式在 JDK 源码里到处都是值得你写代码时模仿。3. 局部内部类只在方法里活的一种3.1 作用域限制与实际使用场景局部内部类定义在方法体或代码块里有一个非常鲜明的特点它的作用域只在所在的花括号内出了这个方法谁都不认识它。因为没有名字在外部可见它天然就不需要访问修饰符也碰不到 static 关键字。public class Outer { public void doSomething() { class LocalHelper { private int x 1; public int getX() { return x; } } LocalHelper helper new LocalHelper(); System.out.println(helper.getX()); } }说实话局部内部类在业务代码里出现的频率极低。我在真实项目里见过它的地方基本都是某个方法内部需要一个有名字的辅助类但又不希望这个类污染类的命名空间。比如一个方法需要同时返回多个结果对象你又不想为这一个方法单独建一个顶层文件就可以用局部类。但从可维护性角度我更推荐用 recordJava 16或者直接建一个私有静态内部类局部类可读性并不好。3.2 effectively final 规则为什么引入了值就不能改局部内部类最出名的限制是它只能捕获事实不可变的局部变量。所谓 effectively final就是变量初始化之后再没被重新赋值。Java 8 之前要求必须显式声明 finalJava 8 之后放宽了只要你没改编译器就默认它是 final。为什么会有这个限制回到编译原理。局部内部类要访问方法的局部变量但局部变量在栈上方法结束后栈帧就没了内部类对象却可能活得更久。所以编译器干脆把局部变量的值复制一份存进内部类对象里。既然是复制你如果在方法后面改了原变量内部类里那份副本还是旧值就会产生两边数值不一致的诡异现象。为了避免这种混乱编译器直接一刀切只要你改了局部变量就不让你编译通过。举一个反面例子public void test() { int x 1; class Local { public void print() { System.out.println(x); // 编译报错x 不是 effectively final } } x 2; }看到没x 2这一步把 x 变得不再 effectively final编译器就会在 println 那行报错。这规则不只针对局部内部类匿名内部类和 lambda 也一样后面会再碰见。理解了这个复制值的机制你就明白为什么只是没再赋值就能通过编译而不是必须写死一个 final 关键字。3.3 局部内部类和匿名内部类的关系很多人会把局部内部类和匿名内部类搞混因为它们在定义位置上是一致的都在方法体里。区别就一个局部内部类有名字匿名内部类没有。匿名内部类可以看成局部内部类的一种极端简写——名字都懒得起了直接 new 接口或父类。它们对变量的捕获规则也完全一致都必须 effectively final。但匿名内部类更常见的原因很简单大多数方法内的临时实现都很短起名字反而比写逻辑更费劲。而且匿名内部类和 lambda 几乎可以无缝替换所以在现代代码里局部内部类越来越像一个偏理论的存在而匿名内部类则活在每一行回调代码里。4. 匿名内部类面试最高频的那一个4.1 本质、限制与编译细节匿名内部类是面试的绝对主角。它没有名字定义的同时完成创建通常用来快速实现一个接口或者继承一个类Runnable r new Runnable() { Override public void run() { System.out.println(hello); } };这个写法背后发生了什么编译后它会生成Outer$1.class这样一个文件类名是编译器随便编的。匿名内部类有几个硬性限制没有类名所以不能写构造方法。想在创建时传参只能借助实例初始化块{}或者从外部条件里提前准备好数据。只能继承一个类或实现一个接口。如果接口或父类有多个抽象方法必须全部实现。在 Java 16 之前不能定义静态成员static final 常量除外。这里有个小技巧匿名内部类里可以用实例初始化块伪造构造逻辑因为初始化块在构造方法之后、构造函数体之前执行虽然不能接参数但你可以在外围已经捕获的变量里做一些赋值。class Base { private String msg; public Base(String msg) { this.msg msg; } } Base b new Base(外部固定参数) { private String extra 匿名类自己的字段; { System.out.println(匿名内部类初始化块执行, extra extra); } public void doSomething() { System.out.println(额外方法); } };不过我要提醒一句匿名内部类如果被声明成Base类型额外的方法doSomething()在外面是调不到的因为引用类型是Base编译器只看静态类型。你看到的变量类型决定了你能调什么方法匿名类里多的方法反射才拿得到正常代码里别指望。4.2 匿名内部类与 lambda 的转换关系这个点现在面试频率极高因为很多公司同时考察Java 基础 新特性。Java 8 之后如果匿名内部类实现的接口恰好是函数式接口只有一个抽象方法就可以简化成 lambdaRunnable r () - System.out.println(hello);很多人以为 lambda 就是语法层面的匿名内部类简写这个说法并不准确。lambda 的底层走的是 invokedynamic 和 LambdaMetafactory和匿名内部类的编译期生成新类是两套完全不同的机制。匿名内部类会生成额外的 .class 文件每个实例都要经历类加载而 lambda 通常由 VM 在运行期动态生成实现体非捕获 lambda 甚至可以复用一个实例内存开销和使用成本都低不少。实际开发里lambda 不仅代码更短性能也通常更好。但要注意lambda 里this指向的是外部类当前实例而不是接口实现类本身匿名内部类里的this指向的是匿名类自己。这是个极其容易踩的坑面试官特别喜欢问lambda 和匿名内部类的 this 有什么区别第 4.4 节我展开说。4.3 典型应用场景监听器、线程、比较器、动态代理匿名内部类在真实项目里最常出现在四个场景。监听器/回调按钮点击、网络请求回调一个接口一个方法临时实现。比如button.addActionListener(new ActionListener() { Override public void actionPerformed(ActionEvent e) { handleClick(e); } });线程new Thread(new Runnable(){...})是老一辈的写法现在基本被 lambda 替代但面试题里依然会出现因为这是考察匿名类和变量捕获最经典的载体。比较器Collections.sort(list, new Comparator(){...})需要多个临时排序规则时特别好用不用单独定义类。动态代理JDK 动态代理里的InvocationHandler实现十个人里有八个写成匿名内部类。这个场景也是把成员捕获、接口实现、方法回调全部串在一起的好例子UserService proxy (UserService) Proxy.newProxyInstance( UserService.class.getClassLoader(), new Class[]{UserService.class}, new InvocationHandler() { Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start System.currentTimeMillis(); Object result method.invoke(target, args); long cost System.currentTimeMillis() - start; System.out.println(方法耗时: cost ms); return result; } } );这里匿名内部类直接捕获了target变量再一次体现了捕获 effectively final 变量的语法。如果你想给代理逻辑动态传不同的 target就会想到把 target 改成某个可变容器或者干脆用一个显式的 InvocationHandler 实现类把 target 作为构造参数传入——这就是为什么真实框架里Proxy.newProxyInstance的第三参数经常不是匿名类而是一个可复用的实现类。4.4 面试官追问匿名类里的 this 和变量捕获把这一节单独拎出来是因为它几乎每年都会出现在面试题里。先说this。匿名内部类的方法里写this指向的是匿名类自己lambda 的表达式体里写this指向的是外部类的当前实例。原因也很简单lambda 本质上只是对外部方法的一小段代码的封装它没有自己的类上下文所有成员访问都还是外部类的。举个例子public class Demo { private String name demo; public void test() { Runnable r1 new Runnable() { private String name inner; Override public void run() { System.out.println(this.name); // inner } }; Runnable r2 () - { // lambda 里不能定义和外部成员同名的局部变量来遮蔽 this.name System.out.println(this.name); // demo }; r1.run(); r2.run(); } }再说变量捕获。匿名内部类和 lambda 都要求捕获的局部变量 effectively final但有一个微妙的差别匿名内部类捕获变量时实际上是把它作为构造参数传给隐藏构造器存成自己的字段而 lambda 捕获变量是通过 invokedynamic 的参数传递方式在运行时绑定。对使用者来说效果相似但面试官若问为什么 lambda 里不能给捕获变量重新赋值你还是得讲到外部栈变量被复制到堆上的实现这一层。5. 静态内部类唯一和外部实例解耦的一种5.1 与成员内部类的本质区别静态内部类static nested class是被 static 修饰的内部类它最大的特点不持有外部类实例引用。没有this$0字段也不依赖外部类对象存活。public class Outer { private static String config global; private String name instance; static class Nested { public void print() { System.out.println(config); // 可以访问外部类静态成员 // System.out.println(name); // 编译错误不能访问外部类实例成员 } } }实例化方式也不同Outer.Nested nested new Outer.Nested();不需要先 new Outer。这也意味着静态内部类完全可以独立存活即使外部类实例被回收它也不受影响。从内存角度它是四种内部类里最安全的一种。面试里最爱问static 内部类和成员内部类的区别标准回答概括成三点静态内部类不持有外部类引用成员内部类持有静态内部类可以定义静态成员成员内部类不行Java 16 之前实例化方式不同new Outer.Nested()对比outer.new Inner()。5.2 经典应用Builder 模式与 Map.Entry静态内部类在真实代码里最出名的应用是Builder 模式。Lombok 的Builder生成的代码就是这个套路手写也很常见public class User { private final String name; private final int age; private User(Builder builder) { this.name builder.name; this.age builder.age; } public static class Builder { private String name; private int age; public Builder name(String name) { this.name name; return this; } public Builder age(int age) { this.age age; return this; } public User build() { return new User(this); } } }这里User的构造方法是 private 的外部只能通过new User.Builder().name(lisi).age(20).build()创建。Builder和User逻辑上可以拆开成两个类但放在一起语义上Builder 只服务于 User这种关系一目了然。用静态内部类而不是成员内部类还有一个额外好处如果Builder是成员内部类它的每个实例都会无端持有User实例引用链式调用过程中这个引用基本是浪费。另一个教科书级应用是Map.Entry。Map 接口内部定义了一个EntryK,V接口用来描述键值对。它定义在 Map 内部是为了强调Entry 是 Map 的一部分而具体实现HashMap.Node同样是一个 static class。设想一下如果 Node 是成员内部类HashMap 里每个 Node 都持有外部 HashMap 对象的引用那 HashMap 被回收时桶数组上的每个 Node 还拖着一个 HashMap 实例这个内存消耗是灾难性的。这种把强关联类型塞进主类型内部的做法在 JDK 源码里到处都是你研究框架源码时不妨专门看看别人是怎么用嵌套类的。5.3 内部类的继承与序列化问题静态内部类可以被外部正常继承但非静态成员内部类的继承有个非常别扭的语法。如果你想写一个子类继承 Outer 的 Inner子类构造器第一行必须是外部实例.super()public class Outer { class Inner { public Inner() {} } } public class SubInner extends Outer.Inner { public SubInner(Outer outer) { outer.super(); // 必须显式传入外部类实例 } }这个outer.super()语法很少见但面试偶尔会考。它的语义是内部类的父类构造器需要一个外部类实例子类必须通过某个外部实例.super()把这个实例传给父类。如果你不知道这个语法直接写super()会得到编译错误。写起来很奇怪但确实符合语法规范。再说序列化。Java 的序列化对非静态内部类很不友好。一个非静态内部类如果想被序列化它除了实现 Serializable还得保证外部类也可序列化而且反序列化时需要恢复this$0引用。因为this$0不是普通字段很多序列化框架根本拿不到它反序列化直接抛异常。我在一个项目里踩过这个坑把包含内部类实现的回调对象写进缓存取出来时 ClassCastException查了半天才发现是内部类反序列化问题。所以远程传输、缓存存放这类对象多用静态内部类要不就直接转 DTO别把内部类对象往序列化协议里丢。6. 面试八股重点与内存泄漏排查6.1 为什么非静态内部类会造成内存泄漏这是内部类相关的最经典面试题也是真实开发里最值得重视的问题。先看一个典型错误代码public class Activity extends AppCompatActivity { private Handler handler new Handler(Looper.getMainLooper()) { Override public void handleMessage(Message msg) { // 更新 UI } }; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); handler.sendEmptyMessageDelayed(0, 60000); } }这段代码的问题handler是匿名内部类非静态所以它持有Activity.this的引用。handler.sendEmptyMessageDelayed会把消息投递到主线程消息队列消息队列里的 Message 又持有了handler。只要消息队列还没执行到那条延迟消息整条链路Message - Handler - Activity就不会断。这时候你按返回键销毁 ActivityGC 也回收不了它内存泄漏就这么产生了。解决办法其实很简单三条路把 Handler 定义成静态内部类再用WeakReferenceActivity弱引用持有外部类在 onDestroy 里调用handler.removeCallbacksAndMessages(null)清掉所有待处理消息使用框架自带的生命周期绑定机制比如协程、生命周期感知组件。不只是 Handler监听器、回调、观察者模式里只要内部类实例比外部类活得更久就该警惕内存泄漏。通用排查思路先看这个内部类实例是否被某个生命周期更长的容器消息队列、集合、单例、静态缓存持有是的话要么改静态内部类 弱引用要么在销毁时机主动解绑。我用 MAT 或者 jmap 分析堆时只要看到this$0字段大量存在基本就能确定这类泄漏。如果你用的是 JDK 17 以上jdk.jfr里也有对象分配相关的监控能力定位起来会更快。6.2 高频面试题与答题思路这块我把近几年面过的、听别人被问过的题整理一下给你一个可以直接背的回答框架。记住面试官要的不只是一个名词而是你能不能把背后的机制讲清楚。Q1内部类有哪几种先分类成员内部类、局部内部类、匿名内部类、静态内部类。再逐个说定义位置、是否持有外部引用、实例化方式。最后补一句编译后的命名规则Outer$Inner.class、Outer$1.class会显得你懂底层。Q2成员内部类为什么不能有 static 方法Java 16 之前因为成员内部类依赖外部类实例才能存在它的对象必须先有外部引用一旦允许静态成员静态成员的加载时机和外部类实例绑定就产生了矛盾。一句话它的生命周期绑定实例静态成员又属于类二者冲突。Q3静态内部类为什么不持有外部类引用从 JVM 角度看静态成员属于类本身不属于某个实例。static class Nested就是这个内部类与外部类实例无关编译器自然不会生成this$0字段。Q4匿名内部类里可以修改外部局部变量吗不可以。局部变量必须 effectively final。原因是编译器把局部变量拷贝进匿名类对象如果原变量可以变拷贝副本就会和原值不同步产生逻辑混乱。这在 Java 8 之前是强制 final8 之后只要没被重新赋值即可。Q5lambda 和匿名内部类有什么区别至少答三点编译产物不同匿名内部类生成独立 class 文件lambda 用 invokedynamicthis语义不同匿名内部类指向自己lambda 指向外部类实例接口约束不同lambda 要求函数式接口匿名类可以用于非函数式接口。加分项lambda 通常没有额外类加载成本非捕获 lambda 可以复用一个实现。Q6内部类可以继承其他类吗可以。内部类本质还是类可以继承父类也可以实现接口。反过来非静态内部类被继承时子类构造器需要用外部实例.super()语法。这个比较冷门知道存在就行。Q7为什么 HashMap 里的 Node 是静态类因为 Node 被桶数组引用时不应该持有 HashMap 实例引用持有的话HashMap 被回收时整个链表的节点还拖着 HashMap 实例内存泄漏隐患。而且 Node 需要被单独创建和使用跟某个具体 HashMap 实例没关系。这也是面试官顺带考察内部类内存模型的经典题。6.3 实战建议我对内部类的三条使用原则最后说点纯经验。这几年我写 Java 代码对内部类的态度可以总结成三句话能用静态就不用非静态能用 lambda 就不用匿名类能用顶层类就不用内部类。第一句解决内存问题。非静态内部类没有收益却多一份引用很多时候根本没必要。比如一个 helper 类不访问外部实例字段却写成成员内部类纯属给自己挖坑。我见过最夸张的一次一个通用工具类里的静态方法内部用到了某个内部类结果整个工具类的实例被前端请求一直持有线上 GC 压力巨大。改成静态内部类之后问题直接消失。第二句解决臃肿问题。匿名类写回调时代码块一大坨lambda 一眼能看清逻辑。但注意lambda 不适合那些需要保持状态的场景比如你要一个持有计数器的回调匿名内部类可以加实例字段lambda 里的局部变量又 effectively final就不好搞。这时候用静态内部类或者独立普通类而不是强行嵌套。第三句是代码组织问题。内部类属于强内聚的小众类如果它超过 100 行请把它挪出去。我见过一个类里放了三个成员内部类每个都有几十行私有方法结果外面看这个类根本分不清哪些逻辑属于外层哪些属于内层。重构时把每个内部类都抽成顶层类配上构造器注入需要的依赖结构瞬间清明。另外还有一个我看过无数次的低级错误在非静态内部类里调用外部类 private 方法没问题但如果你在这个内部类里做了反射、序列化或者把它丢进了某个框架的容器里框架很可能拿到的是Outer$Inner这种无法独立构造的类。Spring 的依赖注入对内部类非常不友好组件扫描时经常扫不到或者实例化失败。所以被框架管理的类尽量不要设计成内部类这类经验是用一次就再也不想踩第二次的。内部类这东西面试考的是底层机制工作考的是分寸感。你把this$0、Outer.this、effectively final、内存泄漏这四件事彻底搞明白剩下的都是怎么用的问题。写代码时多问一句这个内部类拿外部引用有意义吗就能避开 90% 的坑。我最后再分享一个习惯每次写完内部类都跑一下javac后看看生成的 class 文件列表文件名里带不带$带几个$基本能告诉你这个类的嵌套程度是否已经失控了。
返回列表