你知道的越多,不知道的就越多,业余的像一棵小草!
你关注,我们一起精进!你星标,我们便有了更多故事!
业余时间 Java 种草!
编辑:业余草
推荐: https://t.zsxq.com/ R0nBd
为什么spring.factories也下岗了?为什么SpringBoot3.x要废弃它?
从 Java 中的 CAS 看 Kimi K2 大模型与 MuonClip 技术。
写个 Maven 插件装装样子:在 build 阶段干点自己的事
大概是 3 年前,我在微信读书上拜读周志明老师的《深入理解 Java 虚拟机》第三版,当时做了不少留言。今天,有微信群友在书里遇到了我,在群里咨询问题,我也是凭着印象与记忆,献丑了一番。这会空闲下来,我整理成文分享给更多的网友。一起了解一些 Java 内部类:作用、陷阱与内存泄露( Java 内部类:爱你一万年,哪怕内存溢出 )等经验。
Java 内部类的作用
我们先从 Java 的内部类的作用说起。
在 Java 中,内部类(Inner Class)是一种定义在另一个类内部的类。内部类提供了许多有用的功能,包括但不限于以下几点:
访问外部类的私有成员 :内部类可以无条件地访问外部类的所有元素,包括私有成员变量和方法,这使得内部类非常适合用于辅助类的设计。 实现隐藏 :内部类可以对外部隐藏,这样可以更好地封装代码逻辑,减少对外暴露的类数量,提高代码的安全性和可维护性。 多重继承的模拟 :虽然 Java 不支持多继承,但内部类可以通过继承不同的外部类来模拟多重继承的效果。 优化接口实现 :通过匿名内部类,可以简化接口或抽象类的实现,特别是在事件监听等场景中非常常见。
Java 内部类的分类
通过上面内部类的作用中可知,Java 中的内部类通常可以按照下面的种类进行划分。
成员内部类(Member Inner Class)
直接定义在另一个类的内部,并且不加 static 修饰。
其特点是:
静态内部类(Static Nested Class)
与成员内部类相似,定义在另一个类的内部,但用 static 修饰。
其特点是:
局部内部类(Local Inner Class)
定义在方法、构造器或代码块内部的类。这个局部内部类用的场景更少。
其特点是:
匿名内部类 Anonymous Inner Class
定义:没有显式类名,直接通过 new 接口/父类的方式定义。
特点:
回顾或了解了以上内容后,接下来我们进入正题,看看内部类在使用过程中的一些陷阱。
使用内部类的陷阱
尽管内部类有很多优势,但在使用过程中也存在一些潜在的陷阱,尤其是与内存管理和生命周期控制相关的陷阱。
内存泄露(Memory Leak)
非静态内部类(如成员内部类和匿名内部类)会隐式持有外部类的引用 。如果内部类的生命周期长于外部类的生命周期,或者内部类被长时间持有(例如被缓存、线程池引用等),则可能导致外部类无法被垃圾回收器回收,从而引发内存泄露。
难以理解的封装结构
过度使用内部类可能会导致代码结构变得复杂,尤其是嵌套层次过多时,会增加代码的可读性和维护难度。
写到这里,我想起了我刚入职场时,面试官问到我的一个问题。 内部类,可以嵌套多少层? 。
这个问题真不高明,反过来问面试官,面试官也回答不出来。
class Outer {
class Inner1 {
class Inner2 {
class Inner3 {
// 可以继续嵌套...
}
}
}
}
实际上,也很少有人去考究。我觉得可以从下面 4 个方面来回答这个问题。
Java 语法上允许无限嵌套:Java 的语法并没有限制内部类的嵌套层数。 编译器限制:某些 Java 编译器(如 javac)可能对嵌套层数有隐式限制(例如 65535 层,受 .class 文件结构的限制),但这种限制在现实中几乎不会遇到。 可读性与维护性:过度嵌套会导致代码难以理解和维护,通常不建议超过 2~3 层。 JVM 限制:方法调用栈的深度(如递归调用时)可能间接影响嵌套内部类的实例化,但这是运行时问题,而非语法问题。
基本上,说出你的思路即可。下面我们说说内部类对内存回收的影响
内部类对内存回收的影响
由于非静态内部类会持有外部类的引用,如果内部类实例被长期持有,外部类实例将无法被释放,即使它已经不再需要。这种现象在使用匿名内部类时尤为常见,尤其是在 Android 开发中,Handler、Runnable 等对象常常引发内存泄露问题。
下面是一个简单的内存泄露案例,代码展示了匿名内部类如何导致外部类无法被回收。
public class OuterClass {
private Object heavyResource;
public OuterClass() {
this.heavyResource = new Object(); // 假设这是一个占用大量内存的资源
}
public void startBackgroundTask() {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10000); // 模拟长时间运行的任务
} catch (InterruptedException e) {
e.printStackTrace();
}
// 使用外部类的资源
System.out.println("Resource used: " + heavyResource.hashCode());
}
}).start();
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
OuterClass outer = new OuterClass();
outer.startBackgroundTask();
// outer 对象本应在此处被回收,但由于匿名内部类持有其引用,可能导致其无法被回收
}
}
}
上面这个例子中, Runnable 是 OuterClass 的一个匿名内部类,它隐式持有 OuterClass 实例的引用。当我们在 main 方法中循环创建 OuterClass 实例并启动后台任务时,这些实例可能不会被及时回收,从而导致内存占用不断上升。
为了更快速的出现内存泄露,我建议大家可以 new 一个大的 byte 数组,加快异常现象的显现。
如何避免内部类引发的内存泄露
这里,我总结了 3 条经验,大家可以在评论区补充留言评论。
使用静态内部类 :静态内部类不会持有外部类的引用,因此可以避免因内部类生命周期过长而导致的内存泄露问题。 手动解除引用 :在不再需要内部类实例时,显式地将其置为 null,以帮助垃圾回收器回收资源。使用弱引用(WeakReference) :在某些场景下,可以使用 WeakReference来持有外部类的引用,这样即使内部类仍然存在,外部类也可以被回收。
总结
内部类是 Java 中一个强大但很容易被滥用的功能。合理使用内部类可以提高代码的封装性和可读性,但如果忽视其潜在的陷阱,尤其是内存泄露问题,可能会导致严重的性能问题甚至程序崩溃。作为 Java 开发者,我们应当充分理解内部类的工作机制,并在实际开发中谨慎使用,确保程序的健壮性和高效性。
以上,我们明天见!
