一、一句话核心区别(开场定调)
- 非静态内部类(成员内部类) :黏在外部类实例上 。它内部自动藏了一个指向
外部类.this的指针。 - 静态内部类 :脱离外部类实例。就像独立的顶层类,只是借了外部类的命名空间。
- 匿名内部类 :临时的一次性内部类。为了省事,当场定义、当场使用。
二、面试官最关心的"硬核"对比表
| 对比维度 | 非静态内部类 | 静态内部类 | 匿名内部类 |
|---|---|---|---|
| 是否有外部类引用 | ✅ 有(隐式持有) | ❌ 没有 | ✅ 有(若在非静态环境下) |
| 能否独立创建 | 不能,必须先有外部类实例 new Outer().new Inner() |
能,直接 new Outer.Inner() |
不能,必须依附于父类/接口的实例化 |
| 能否访问外部类非静态成员 | ✅ 可以直接访问 | ❌ 不能,只能访问静态成员 | ✅ 可以直接访问(若定义在非静态方法内) |
| 能否定义静态成员 | ❌ 不能(除了编译期常量) | ✅ 可以 | ❌ 不能 |
| 构造器 | 有明确构造器 | 有明确构造器 | 没有构造器(编译器生成匿名构造器) |
三、"生命周期与内存泄漏"陷阱
"那你实际开发中怎么选?注意什么?"
黄金法则:
如果内部类不需要访问外部类的实例变量,一律写成
static(静态内部类)。
原因(背下来) :
非静态内部类持有外部类引用,会导致外部类无法被GC回收 。例如在Activity里写一个非静态Handler,如果消息队列还没处理完,Activity就泄露了。具体分析
静态内部类 + 弱引用 是解决内存泄漏的标准套路。
四、匿名内部类的两个致命细节
- 只能捕获
final或"事实不可变"的局部变量 (Java 8以后叫Effectively Final)。为什么?因为它把外部变量的值拷贝进来了,为了防止数据不一致,不允许你修改。 - 匿名内部类没有类名 ,所以不能定义静态成员 ,也不能写显式构造器(只能用实例代码块
{}来模拟构造)。
五、使用场景
- 静态内部类 :写 Builder(建造者模式)、工具类、或与外部类逻辑强关联但不需要持有引用的组件(如
HashMap.Node)。 - 非静态内部类 :很少用,除非是"外部类的一部分零件",比如
Car.Engine,且Engine必须知道开的是哪辆车。 - 匿名内部类 :一次性使用的回调 (比如点击事件
setOnClickListener)、Runnable线程、或临时比较器Comparator。
关于 隐式持有 具体分析
非静态内部类
java
public class Outer {
public class Inner{
// 只有一个空构造
public Inner(){}
}
}
编译后会生成 两个
.class文件:
Outer.class------ 外部类的字节码。Outer$Inner.class------ 非静态内部类 Inner 的字节码($ 是分隔符,表示内部类)。
反编译 Outer$Inner.class 得到的真实代码:
java
public class Outer$Inner {
// 这行引用是编译器强制生成的,逃不掉!
final Outer this$0;
public Outer$Inner(Outer var1) {
this.this$0 = var1; // 强制把外部类实例赋值给 this$0
super(); // 然后才执行你写的空代码
}
}
结论 :写 new Outer().new Inner() 时,底层字节码其实是 new OuterInner(outerInstance)。不管你用不用 外部类的成员变量,这个 this0 引用都会指向 Outer。即 隐式持有外部类
匿名内部类
java
public class Outer {
public void test() {
// 这是一个匿名内部类,即使 run() 里是空的
Runnable r = new Runnable() {
@Override
public void run() {
// 什么都不做,也不访问外部变量
}
};
}
}
编译后会生成 两个
.class文件:
Outer.class外部类Outer$1.class匿名内部类
Outer$1.class 匿名内部类 反编译后:
java
class Outer$1 implements Runnable {
final Outer this$0; // 隐式持有!
Outer$1(Outer var1) {
this.this$0 = var1; // 构造器硬塞进来的!
super();
}
public void run() {
// 你写的空代码
}
}
只要是 new 在非静态上下文(普通方法里),匿名内部类的构造器第一个参数永远藏着外部类 this。 即 隐式持有外部类。
静态内部类
java
public class Outer {
// 注意这里:加了 static
public static class StaticInner {
public StaticInner() {
// 里面依然啥也不干
}
}
}
编译后 会 生成两个
.class文件:
Outer.class外部类Outer$StaticInner.class静态内部类
反编译 Outer$StaticInner.class 后的真实代码:
java
public class Outer$StaticInner {
// 注意看!!!【这里没有】 final Outer this$0;
public Outer$StaticInner() {
super(); // 只有这一行,干干净净
}
}
静态内部类 不 隐式持有 外部类对象