- 0
- 345 words
上篇我们聊了成员内部类和静态内部类,这篇搞定剩下的两个:局部内部类和匿名内部类。
这两种写法和前面最大的区别是什么?它们都在方法体内部定义——作用域被限制在了方法里,外面完全看不见。
一、局部内部类(Local Inner Class)
1.1 定义在方法里的类
局部内部类写起来就像这样:
public class Outer {
public void sayHello() {
// 局部内部类:定义在方法里面
class LocalInner {
private String name;
public LocalInner(String name) {
this.name = name;
}
public void greet() {
System.out.println("你好," + name);
}
}
// 只能在方法内使用
LocalInner inner = new LocalInner("小明");
inner.greet();
}
}
看到了吧?LocalInner 类定义在了 sayHello() 方法里面。它的作用域仅限于这个方法——方法跑完了,外面谁也找不到它。
1.2 局部内部类的特性
- 作用域仅限于所在的方法,方法外完全不可见
- 可以访问外部类的所有成员(跟成员内部类一样)
- 可以访问方法内的
final或 有效 final 的局部变量 - Java 8+ 要求局部变量是「有效 final」(即变量值不改变,即使没写
final关键字)
来看一个具体的:
public class Outer {
private int num = 100;
public void process() {
int localNum = 50; // 有效 final——值没有改变
class Processor {
public void print() {
// 访问外部类成员 ✅
System.out.println("外部类成员 num = " + num);
// 访问方法内的局部变量 ✅(必须是有效 final)
System.out.println("局部变量 localNum = " + localNum);
}
}
new Processor().print();
}
}
那为什么局部变量必须是 final 或有效 final?原因跟内存有关——方法执行完栈帧就销毁了,但内部类对象可能还在堆里活着。Java 其实把局部变量的值复制了一份到内部类对象中,如果这个变量还能被修改,就会出现「复制品跟原件不一致」的 bug。所以干脆只让内部类访问不可变的变量,从根源上避免问题。
1.3 什么时候用局部内部类?
说实话,局部内部类在日常写业务代码时用得不多。最典型的场景是:你有一个方法,里面需要创建一个稍微复杂一点的类来完成某个局部任务,但这个类没啥复用价值,写在外面又显得多余。
比如你写一个读取文件的工具方法,里面需要一个临时数据结构:
public void readConfig() {
// 局部内部类:只在读取配置时临时用一下
class ConfigField {
String key;
String value;
int lineNumber;
ConfigField(String key, String value, int lineNumber) {
this.key = key;
this.value = value;
this.lineNumber = lineNumber;
}
}
// 解析过程...
List<ConfigField> fields = parseLines();
// ...
}
但这种场景……说实话直接用 Map 或者 Record(Java 16+)就完事了。所以局部内部类我个人的感觉是:知道有这么个东西就行,面试不考,实战用得少。
二、匿名内部类(Anonymous Inner Class)
这个就不一样了——匿名内部类在实际开发中非常常见。
匿名内部类,顾名思义,没有名字的内部类。它在声明的同时直接创建对象,而且只能使用一次。
2.1 基本语法
// 写法:
new 父类或接口() {
// 实现或重写的方法
};
来看具体的例子。假设有一个 Animal 接口:
interface Animal {
void speak();
}
// 以前你得这么写:
class Dog implements Animal {
@Override
public void speak() {
System.out.println("汪汪汪");
}
}
Animal dog = new Dog();
// 匿名内部类版本:
Animal cat = new Animal() {
@Override
public void speak() {
System.out.println("喵喵喵");
}
};
看到了吧?不用单独定义一个 Cat 类,直接 new Animal() { ... } 就搞定了一个「匿名的 Animal 实现」。这个类没有名字,只在这里用一次。
2.2 匿名内部类 + 抽象类
不仅是接口,抽象类也可以:
abstract class Logger {
public abstract void log(String msg);
}
public class App {
public static void main(String[] args) {
// 匿名内部类实现抽象类
Logger logger = new Logger() {
@Override
public void log(String msg) {
System.out.println("[LOG] " + msg);
}
};
logger.log("应用启动了");
}
}
2.3 匿名内部类与回调
匿名内部类最经典的应用就是 回调(Callback)。你写一个工具方法,需要让调用者决定某个步骤怎么做——这时候传一个匿名内部类进去就非常自然。
// 定义一个回调接口
interface Callback {
void onSuccess(String result);
void onFailure(String error);
}
// 模拟一个网络请求工具类
class HttpUtil {
public static void get(String url, Callback callback) {
if (url == null || url.isEmpty()) {
callback.onFailure("URL 不能为空");
return;
}
// 模拟请求成功
System.out.println("正在请求: " + url);
callback.onSuccess("响应结果: 200 OK");
}
}
// 使用匿名内部类做回调
public class Test {
public static void main(String[] args) {
HttpUtil.get("https://api.example.com/user", new Callback() {
@Override
public void onSuccess(String result) {
System.out.println("✅ 成功: " + result);
}
@Override
public void onFailure(String error) {
System.out.println("❌ 失败: " + error);
}
});
}
}
这个写法眼熟不?学 Android 开发的更是天天见——Button.setOnClickListener(new OnClickListener() { ... }) 就是标准的匿名内部类。
2.4 与 Lambda 表达式的关系
Java 8 引入 Lambda 表达式后,如果一个接口只有一个抽象方法(即函数式接口,用 @FunctionalInterface 标记),就可以用 Lambda 替代匿名内部类:
// 匿名内部类方式
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("线程运行中...");
}
};
// Lambda 表达式方式
Runnable r2 = () -> System.out.println("线程运行中...");
Lambda 比匿名内部类更简洁、可读性更强,而且 Java 对 Lambda 做了性能优化(invokedynamic 指令),不会像匿名内部类那样每次调用都生成一个 .class 文件。
所以现在写代码:能用 Lambda 的就别用匿名内部类。但 Lambda 不能覆盖所有情况——当接口有多个抽象方法,或者你需要 this 指向内部类对象而不是外部的时候,匿名内部类还是得用。
三、完整对比:四种内部类
| 类型 | 定义位置 | 持有外部类引用 | 能否定义静态成员 | 使用频率 |
|---|---|---|---|---|
| 成员内部类 | 类中 | ✅ | ❌ | ⭐⭐⭐ |
| 静态内部类 | 类中(static) | ❌ | ✅ | ⭐⭐⭐⭐ |
| 局部内部类 | 方法内 | ✅ | ❌ | ⭐ |
| 匿名内部类 | 方法内(表达式) | ✅ | ❌ | ⭐⭐⭐ |
四、内部类的一些”坑”
4.1 内存泄漏风险
成员内部类持有外部类引用,这听起来方便,但暗藏风险:如果内部类对象生命周期比外部类长,外部类就无法被 GC 回收。
public class BigDataProcessor {
private byte[] hugeData = new byte[1024 * 1024 * 100]; // 100MB
public class Processor {
public void process() {
System.out.println("处理数据中...");
}
}
public Processor getProcessor() {
return new Processor(); // 返回内部类对象
}
}
// 使用
BigDataProcessor bdp = new BigDataProcessor();
BigDataProcessor.Processor p = bdp.getProcessor();
bdp = null; // 以为 bdp 可以回收了?
// 但实际上 p 还持有 bdp 的引用,100MB 内存释放不了!
这就是经典的内存泄漏场景。解决方案?如果内部类不需要访问外部类的实例成员,就声明为 静态内部类——这样就不持有外部引用,不会有泄漏问题。
4.2 序列化问题
内部类默认不实现 Serializable,而且编译器生成的构造器和字段名是不可预测的。如果你需要序列化内部类对象,最好改成静态内部类,或者干脆提取成顶层类。
五、小结
- 局部内部类定义在方法体里,作用域局限在方法内,只能用有效 final 的局部变量
- 匿名内部类没有名字,声明即创建对象,常用于回调、事件监听、一次性实现
- 匿名内部类可以用 Lambda(函数式接口)替代,更简洁
- 成员内部类注意内存泄漏——不需要访问外部实例成员时用静态内部类
- 四种内部类的
.class文件命名:Outer$类型名.class
内部类就到这里了,两篇讲完。下一篇我们开始聊 Object 类——它是所有类的老祖宗,toString()、equals()、hashCode() 这些天天打交道的方法,你真的搞懂了吗?