问题
什么是匿名内部类?如何访问其外部定义的变量?
答案
核心概念
匿名内部类(Anonymous Inner Class)是一种没有名字的内部类,在定义的同时直接创建实例。它通常用于简化代码,特别是在需要实现接口或继承类但只使用一次的场景。
基本语法
// 实现接口的匿名内部类
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类实现 Runnable");
}
};
// 继承类的匿名内部类
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("匿名内部类继承 Thread");
}
};
语法结构:
new 父类构造器(参数) 或 接口() {
// 匿名内部类的类体
// 可以重写方法、定义新方法和字段
};
典型使用场景
1. 事件监听器(GUI 编程)
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("按钮被点击");
}
});
2. 线程创建
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("执行异步任务");
}
}).start();
3. 集合排序
List<String> list = Arrays.asList("banana", "apple", "cherry");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.length() - s2.length(); // 按长度排序
}
});
访问外部变量的规则
匿名内部类可以访问外部的变量,但有严格的限制:
1. 访问外部类的成员变量(无限制)
public class Outer {
private int outerField = 100;
private static int staticField = 200;
public void test() {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(outerField); // ✅ 可以访问实例变量
System.out.println(staticField); // ✅ 可以访问静态变量
outerField = 999; // ✅ 可以修改
}
};
runnable.run();
}
}
2. 访问局部变量(必须是 final 或 effectively final)
关键限制:匿名内部类访问的局部变量必须是 final 或 事实上的 final(effectively final,即初始化后未被修改)。
public void test() {
int localVar = 10; // 事实上的 final(未被修改)
final int finalVar = 20; // 显式声明 final
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(localVar); // ✅ 可以访问
System.out.println(finalVar); // ✅ 可以访问
// localVar = 100; // ❌ 编译错误:无法修改
}
};
// localVar = 30; // ❌ 如果在这里修改,上面的访问会编译错误
}
为什么局部变量必须是 final?
本质原因:变量捕获机制(Variable Capture)
- 局部变量的生命周期:方法执行完毕后,局部变量在栈上被销毁
- 匿名内部类的生命周期:对象可能在方法返回后继续存在
- 实现机制:编译器会将局部变量的值复制到匿名内部类的字段中
// 源代码
public void test() {
int x = 10;
Runnable r = new Runnable() {
public void run() {
System.out.println(x);
}
};
}
// 编译器生成的等价代码(简化)
class Outer$1 implements Runnable {
final int val$x; // 编译器生成的字段,存储 x 的副本
Outer$1(int x) {
this.val$x = x;
}
public void run() {
System.out.println(val$x); // 访问的是副本
}
}
如果允许修改:
- 外部的
x和内部类的val$x是两个独立的变量 - 修改一个不会影响另一个,导致数据不一致
- 因此 Java 强制要求
final,确保值不可变
完整示例
public class AnonymousClassDemo {
private int outerField = 100;
public void demonstrate() {
final int finalLocal = 10;
int effectivelyFinal = 20;
// 匿名内部类实现接口
Runnable runnable = new Runnable() {
private int innerField = 5; // 匿名内部类自己的字段
@Override
public void run() {
// 访问外部类的成员变量
System.out.println("外部类字段: " + outerField);
outerField = 200; // 可以修改
// 访问局部变量(必须是 final 或 effectively final)
System.out.println("final 局部变量: " + finalLocal);
System.out.println("effectively final 局部变量: " + effectivelyFinal);
// 访问自己的字段
System.out.println("内部类字段: " + innerField);
// ❌ 不能修改局部变量
// effectivelyFinal = 30; // 编译错误
}
};
runnable.run();
}
public static void main(String[] args) {
new AnonymousClassDemo().demonstrate();
}
}
匿名内部类 vs Lambda 表达式
Java 8 引入 Lambda 表达式后,很多匿名内部类可以简化:
// 匿名内部类
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
// Lambda 表达式(更简洁)
Runnable r2 = () -> System.out.println("Hello");
区别:
- this 指向:
- 匿名内部类的
this指向内部类实例 - Lambda 的
this指向外部类实例
- 匿名内部类的
- 编译产物:
- 匿名内部类生成独立的
.class文件(如Outer$1.class) - Lambda 使用
invokedynamic指令,不生成额外类文件
- 匿名内部类生成独立的
- 使用限制:
- Lambda 只能用于函数式接口(只有一个抽象方法的接口)
- 匿名内部类可以实现任意接口或继承类
匿名内部类的限制
- 不能定义构造器:因为没有类名,无法声明构造器(但可以使用实例初始化块)
- 不能是静态的:匿名内部类不能声明为
static - 只能实现一个接口或继承一个类:不能同时实现多个接口
// ✅ 使用实例初始化块
Runnable r = new Runnable() {
{
// 实例初始化块,相当于构造器
System.out.println("初始化匿名内部类");
}
@Override
public void run() {
System.out.println("执行任务");
}
};
实际开发建议
- 优先使用 Lambda:对于函数式接口,Lambda 更简洁
- 复杂逻辑用命名类:如果逻辑复杂或需要复用,定义独立的类
- 注意变量捕获:理解 effectively final 的限制,避免编译错误
- 避免过度嵌套:匿名内部类嵌套会降低代码可读性
面试答题要点
- 定义:没有名字的内部类,定义时直接创建实例
- 使用场景:事件监听、线程创建、一次性接口实现
- 访问规则:
- 可以访问外部类的所有成员(包括 private)
- 访问局部变量必须是 final 或 effectively final
- 原理:编译器通过变量捕获(值复制)实现局部变量访问
- 与 Lambda 对比:Lambda 更简洁,但只适用于函数式接口
这道题考察对 Java 内部类机制的理解,以及闭包(Closure)概念的掌握。