基础不牢,地动山摇
家好,这里是后端基础拾光集。
在上一篇 Lambda 的文章《Lambda 与函数式接口|别只会复制 ()->{},底层规则和坑一次性讲清》我们提到:Lambda 就是用来简化匿名内部类的。 很多小伙伴看完 Lambda 之后可能会有一些疑问:匿名内部类到底是什么?内部类还有哪些种类?什么时候不能用 Lambda,只能写匿名内部类?其实平时开发中我们有时候也会一头雾水,今天我们一起来看看。
Java 允许把类写在另一个类的内部,这就是内部类。内部类一共分 4 种:
- 成员内部类
- 静态内部类(嵌套类)
- 局部内部类
- 匿名内部类
一、生活化类比
们把外部类想象成一套大房子 。 房子可以有多种附属物件,这 4 类物件,对应 4 种内部类。核心判断标准:这个物件是否依附「这套房子实体」而存在。
- 成员内部类(非静态内部类):房子自带内置衣帽间 衣帽间属于这套特定房子实例 的一部分,必须先建好这套房子,衣帽间才存在 ;不能凭空单独造一个衣帽间。 衣帽间能自由访问这套房子里所有东西,包括主卧的私人物品(外部类 private 成员)。 ⚠️ 重点隐患:衣帽间会一直记住自己属于哪一套房子。如果房子本该拆除,但衣帽间还被别的地方引用,房子就无法被回收,造成内存泄漏。
- 静态内部类(静态嵌套类):放在房子说明书里的独立收纳盒图纸 图纸只是归类写在房子的文档里,不需要建好房子实体,图纸本身就能直接拿出来使用 。 图纸只能查看房子的公共通用参数(外部 static 静态成员),不能直接进入某一套建好的房子、拿房间里独有的实例物品。
业务开发写 DTO 最常用,没有绑定房子实例,不存在内存泄漏风险。
- 局部内部类:装修工人临时带进房间的一次性收纳箱 只在装修这个房间(方法执行) 的阶段临时使用;装修完工(方法执行结束),箱子直接作废,走出这个房间就不能用了。 业务代码极少手写,看懂概念即可。
- 匿名内部类:装修现场临时钉的简易工作台 没有固定名称,现场搭建、当场使用,不会重复复用。Java8 之前写 Runnable、回调逻辑全靠它。
区分 Lambda: 如果工作台只需要做一件事,现在可以简化写成 Lambda; 如果工作台要同时做好几件事、要改造已有家具(继承父类),Lambda 做不到,只能用匿名内部类。
一句话总结核心区分点:是否依赖外部类实例 成员内部类依附房子实体而生;静态内部类只是归类放在房子名下,独立存在。
二、四类内部类逐个说清
1. 成员内部类(非静态内部类)
定义在外部类的成员位置,没有 static 修饰。
✅特点
- 不能脱离外部实例单独创建。必须先创建外部类对象,才能 new 成员内部类对象
- 可以直接访问外部类所有成员,包括 private 私有变量
- 隐式持有外部类实例引用,极易引发内存泄漏
- Java16 之前,成员内部类里面不能定义 static 变量、static 方法
- 容易造成内存泄漏:只要
CloakRoom对象活着,House对象通常也活着。
csharp
public class House {
//外部私有成员
private String owner = "房主小明";
//成员内部类,衣帽间
class CloakRoom{
public void showOwner(){
//直接访问外部类private成员
System.out.println(owner);
}
}
public static void main(String[] args) {
// 必须先new外部类,再new内部类
House house = new House();
House.CloakRoom cloakRoom = house.new CloakRoom();
cloakRoom.showOwner();
}
}
适合场景:这个内部类天然依赖外部实例状态,比如外部对象是"订单",内部类是"订单明细"。
2. 静态内部类(静态嵌套类 static class)
带 static 修饰,写在外部类里面,业务开发高频使用,比如静态 DTO。 ✅特点
- 不需要外部类实例,直接 new,不持有外部对象引用
- 仅能访问外部类的 static 静态成员,无法直接访问外部实例变量
- 不会带来内存泄漏风险,开发优先选静态内部类
typescript
public class House {
private static String address = "阳光小区";
private String owner = "房主小明";
//静态内部类:独立储物箱
static class StorageBox{
public void showAddress(){
System.out.println(address);
//System.out.println(owner); // ❌无法访问外部非静态成员
}
}
public static void main(String[] args) {
//无需外部类实例,直接创建
House.StorageBox box = new House.StorageBox();
box.showAddress();
}
}
适合场景:
- Builder 模式里的
Builder。 - 工具类、辅助类。
- 与外部类有关联,但不需要外部实例状态。
- 想避免内存泄漏时,优先考虑它。
开发小贴士:能静态就静态 。项目里写静态内部 DTO,永远优先
static class,不要写成成员内部类。
3. 局部内部类(方法里的临时工)
定义在方法内部 ,作用域仅限当前方法,项目实战极少写。
csharp
public class House {
public void test(){
//局部内部类,仅在test方法内部生效
class TempBox{
public void print(){
System.out.println("临时收纳盒");
}
}
TempBox tempBox = new TempBox();
tempBox.print();
}
}
特点:
- 作用域只在当前方法/代码块内。
- 可以访问外部类成员。
- 可以访问方法里的局部变量,但变量必须是
final或"事实 final"。 - 不能有访问修饰符,比如
public、private。 - 用得很少,因为 Lambda 和匿名内部类已经覆盖大多数场景。
适合场景:某个复杂方法内部需要一个小型辅助类,但不想污染外部命名空间。
⚠️限制:只能在当前方法中使用,变量要求隐式 final。业务开发基本不会用到,了解概念即可。
4. 匿名内部类(Lambda 前身,重点!)
没有类名,定义的同时直接实例化,可以实现接口、继承父类。 Java8 之前写线程、回调最常用。
csharp
interface Task{
void runTask();
}
public class Demo {
public static void main(String[] args) {
//匿名内部类,没有类名,一次性使用
Task task = new Task(){
@Override
public void runTask() {
System.out.println("执行任务");
}
};
task.runTask();
}
}
👉对比 Lambda 写法(简化后)
ini
Task task = () -> System.out.println("执行任务");
特点:
- 没有类名。
- 通常只使用一次。
- 可以继承类或实现接口。
- 可以有字段、实例初始化块。
- 可以重写多个方法。
- 在实例方法中,匿名内部类通常隐式持有外部类实例。
- 匿名内部类里的
this指匿名类自己,不是外部类。
适合场景:
- 需要继承抽象类/具体类。
- 接口有多个抽象方法。
- 需要自己的字段保存状态。
- 需要实例初始化块。
- 需要
this指向匿名类自身。
三、 匿名内部类 vs Lambda(面试高频)
- 匿名内部类:可以继承普通类、实现任意接口 ,可以写多个方法;
this指向匿名内部类自己 - Lambda:只能用于函数式接口(只有一个抽象方法) ;
this指向外层外部类 - 匿名内部类可以有字段,Lambda 没有自己的字段。
- 匿名内部类可以重写接口默认方法,Lambda 不行。
重点:Lambda 只能用于函数式接口。匿名内部类可以用于接口、抽象类、具体类 。如果需要重写多个方法,不能用 Lambda,只能用匿名内部类。
⚠️开发高频踩坑点
坑 1:成员内部类隐式持有外部类引用,内存泄漏
非静态内部类、匿名内部类,会偷偷持有外部类实例。 如果内部类对象生命周期比外部类更长(放到线程池、缓存、异步任务),外部类对象无法被 GC 回收,造成内存泄漏。
Lambda 也会泄漏
csharp
public class Activity {
private byte[] big = new byte[100 * 1024 * 1024];
static List<Runnable> TASKS = new ArrayList<>();
void onCreate() {
TASKS.add(() -> doSomething()); // 捕获 this
}
void doSomething() {
}
}
() -> doSomething() 等价于捕获了当前 Activity 实例。静态集合长期持有 Lambda,Activity 就回收不了。
✅修复方案:
- 业务 DTO 优先使用static 静态内部类,不持有外部引用
- 异步任务里尽量避免直接使用匿名内部类捕获外部大对象
坑 2:变量捕获要求:局部变量必须隐式 final
匿名 / 局部内部类访问方法里的局部变量,变量必须是 final 或者有效 final。 简单讲:变量一旦赋值之后不能再修改,否则编译报错。 Lambda 也遵守这条规则。
坑 3:this关键字容易混淆
成员内部类里this代表内部类;Outer.this才代表外部类实例。 匿名内部类的 this 指向匿名类;Lambda 中的 this 是外层类。
坑 4:很多新手分不清内部类和嵌套类
严格定义:static 的叫静态嵌套类,不属于 "内部类";不带 static 才叫内部类。 日常口语大家会统一叫内部类,面试区分概念会问到。
🧪面试高频考点
Q1:成员内部类能不能有静态方法?
A:Java16 之前:不允许;Java16 及以后语法放开,但开发依然不推荐。
Q2:静态内部类和成员内部类最大区别?
A:静态内部类不依赖外部实例,不持有外部引用;成员内部类必须依托外部对象,持有外部类引用,存在内存泄漏风险。
Q3:匿名内部类和 Lambda 的区别,什么时候不能替换 Lambda?
A:Lambda 只能用于函数式接口,不能继承类,不能有字段,this 指向外部;匿名内部类可用于接口/抽象类/具体类,可多方法、有字段,this 指向自身。如果需要继承普通类、重写多个方法,只能使用匿名内部类。
Q4:内部类可以使用哪些访问修饰符?外部类呢?
A:内部类可以用 public/protected/default/private 全部四种;外部类只能 public、default。
Q5:内部类内存泄漏怎么来的?
答:非静态内部类、匿名内部类、捕获 this 的 Lambda 可能隐式持有外部实例。如果被静态集合、单例、线程、Handler、监听器等长生命周期对象引用,外部实例就无法回收。
刚好承接上一篇访问修饰符的知识点。
✅总结避坑清单
- 静态内部类:不持有外部实例,业务写静态 DTO、数据载体,优先static 静态嵌套类,规避内存泄漏。
- 成员内部类:依赖外部实例,慎用,非必要不要写,容易产生隐式引用,内存泄漏。
- 匿名内部类:仅在多方法重写、继承普通类场景使用;函数式接口优先 Lambda 简化代码。
- 局部内部类:项目极少使用,看懂即可,业务尽量不写。
- 匿名内部类:一次性实现,可继承类、多方法、有字段。捕获局部变量,变量必须有效 final,不能二次修改。
- Lambda:函数式接口简写,单方法优先用,但不是匿名内部类的完全替代。
- 内存泄漏:非静态内部类、匿名内部类、捕获 this 的 Lambda,别被长生命周期对象长期持有。
一句话总结:
非静态内部类带外部,静态内部类不带;Lambda 只管单方法,匿名内部类才是多面手;长生命周期别抓短生命周期,注册了就要注销。
📝本文属于专栏【Java 后端基础拾光集】,生活化讲解 Java 基础,持续更新面试知识点。 公众号【Java后端程序媛手记】同步更新 Java 底层原理、后端线上踩坑实录,系列文章整理成合集,方便系统学习。