内部类(成员/静态/局部/匿名)
很多同学第一次看到"类里面还能写类"的时候,觉得这是多此一举。但当你真正读一些开源框架源码——Spring 的 CglibAopProxy、Guava 的 Maps 工具类、JDK 自身的 ArrayList 迭代器实现——你会发现内部类无处不在。
我自己写代码也经历过这个过程:从"内部类是什么"到"什么时候该用它"。今天把四种内部类的用法和坑点全部梳理清楚。
一、为什么需要内部类
内部类存在的根本原因:逻辑上紧密相关、只在一个地方使用的类,不需要单独成为一个文件。
【直观类比】
想象一篇文章里的脚注。脚注和正文紧密相关,只在这篇文章里出现,不需要单独成册。但如果把脚注写在正文里,又会影响可读性。内部类就是这个"脚注"——在需要的地方写清楚,但又不会污染外部命名空间。
二、成员内部类
2.1 基本用法
成员内部类是定义在外部类的成员位置(字段或方法之间)的类:
public class Outer {
private String outerField = "outer";
// 成员内部类
public class Inner {
private String innerField = "inner";
public void accessOuter() {
// 可以直接访问外部类的所有成员(包括 private)
System.out.println(outerField);
System.out.println(Outer.this.outerField); // 显式引用外部类实例
}
}
}
2.2 关键特性
- 依附于外部类实例:必须有外部类对象,才能创建内部类对象
- 可以访问外部类所有成员:包括
private
- 编译产物:
Outer$Inner.class
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner(); // 注意语法:outer.new Inner()
⚠️
创建成员内部类对象时,必须先有外部类实例:outer.new Inner(),而不是 new Inner()。这是因为内部类隐式持有外部类的引用。
三、静态内部类
3.1 基本用法
静态内部类和成员内部类的区别,只在一个关键字 static:
public class Outer {
private static String staticField = "static outer";
private String instanceField = "instance outer";
public static class StaticInner {
public void access() {
// 只能访问外部类的 static 成员
System.out.println(staticField);
// System.out.println(instanceField); // 编译报错!
}
}
}
3.2 关键特性
- 不依赖外部类实例:可以
new Outer.StaticInner() 直接创建
- 只能访问外部类的 static 成员:不能访问实例字段
- 编译产物:
Outer$StaticInner.class
- 用途:如果内部类不需要访问外部类实例,就用
static,这样避免无谓的外部类引用
// 创建静态内部类,不需要外部类实例
Outer.StaticInner inner = new Outer.StaticInner();
💡
当你发现成员内部类从来不访问外部类实例时,应该改成静态内部类。这样更安全(不会意外持有外部类引用)、更高效(不需要创建外部类对象)。
四、局部内部类
4.1 基本用法
局部内部类定义在方法内部:
public class Outer {
public void someMethod() {
final int localVar = 100; // effectively final
// 局部内部类
class LocalInner {
public void print() {
System.out.println("访问局部变量: " + localVar);
}
}
LocalInner inner = new LocalInner();
inner.print();
}
}
4.2 关键特性
- 作用域局限在方法内部:出了方法就不可见
- 可以访问方法的局部变量:但必须是
final 或"有效 final"(JDK 8+)
- 编译产物:
Outer$1LocalInner.class(带编号,因为类名不冲突)
public void someMethod() {
int localVar = 100; // effectively final in JDK 8+
class LocalInner {
public void print() {
// localVar 在编译时被捕获为常量
System.out.println(localVar);
}
}
localVar = 200; // 这里改了,局部内部类里看到的还是 100
// 编译报错!localVar 被局部内部类引用后,不能再修改
}
⚠️
局部内部类引用的局部变量必须是 final 或 effectively final(JDK 8+)。原因:局部内部类实例可能比方法存活更久(被返回、存储到某处),如果局部变量在方法返回后消失,而内部类还在访问它,就会出问题。编译器通过"复制"变量的方式解决这个问题——把局部变量的值复制到内部类里,所以变量不能被修改。
五、匿名内部类
5.1 基本用法
匿名内部类没有名字,定义和实例化一起完成:
public interface Runnable {
void run();
}
public class Outer {
public void create() {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类实现");
}
};
r.run();
}
}
5.2 关键特性
- 没有名字:类定义和实例化一起完成
- 必须是接口或抽象类的实现:不能 new 一个具体类
- 只能访问外部的 final 或 effectively final 变量
- 编译产物:
Outer$1.class、Outer$2.class(按定义顺序编号)
- 常见用途:回调、事件监听、线程任务
// 常见用法:线程
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程任务");
}
}).start();
// JDK 8 以后,可以用 Lambda 简化:
new Thread(() -> System.out.println("Lambda 写法")).start();
💡
匿名内部类的常见翻车点:过度使用。如果匿名内部类超过 20 行,考虑改成具名内部类或独立类,提高可读性。
六、对比总结
七、作用域与遮蔽
7.1 作用域
- 成员内部类:整个外部类都可以访问
- 静态内部类:同成员内部类
- 局部内部类:仅在定义它的方法内
- 匿名内部类:仅在定义它的表达式内
7.2 变量遮蔽
如果内部类和外部类有同名字段,内部类里默认访问的是自己的版本:
public class Outer {
String field = "outer";
public class Inner {
String field = "inner"; // 遮蔽了外部类的 field
public void print() {
System.out.println(field); // "inner" — 内部类自己的
System.out.println(Outer.this.field); // "outer" — 显式指定外部类
}
}
}
八、生产避坑
8.1 内存泄漏风险
成员内部类会隐式持有外部类引用。如果内部类里有长时间存活的对象(比如被 static 持有、被放入缓存),外部类对象就不会被 GC 回收:
// 错误写法:静态集合持有了成员内部类
public class Outer {
private static List<Inner> cache = new ArrayList<>(); // 危险!
public class Inner {
String data;
}
}
解决方案:使用静态内部类,它不持有外部类实例引用。
8.2 序列化问题
成员内部类和匿名内部类默认不能独立序列化(因为依赖外部类实例),如果需要序列化,考虑:
- 实现
Externalizable
- 将内部类改为 static
- 使用内部类作为独立的 DTO 类
【学习小结】
四种内部类的核心区别在于作用域和外部依赖。成员内部类持有外部类引用,适合需要访问外部类所有成员的场景;静态内部类不持有引用,适合工具类场景;局部内部类和匿名内部类作用域最小,适合一次性使用的回调和策略。生产中最容易踩的坑是:不需要外部引用时用了成员内部类,导致内存泄漏。