Lambda 表达式原理
Lambda 是 Java 8 最重磅的特性之一。学的时候大家都会写 list.forEach(x -> System.out.println(x)),但很多人不知道它底层是怎么工作的。
我之前面试过一个人,写 Lambda 写得飞起,但被问到"Lambda 表达式编译后变成什么"就答不上来了。这很正常,因为 Lambda 的实现机制比较复杂,涉及 JVM 的 invokedynamic 指令。
今天我们就来从字节码层面把 Lambda 彻底讲透。
一、Lambda 基础回顾
1.1 Lambda 的几种形式
// 形式1:表达式 lambda(单表达式)
list.forEach(x -> System.out.println(x));
// 形式2:语句块 lambda(多语句)
list.forEach(x -> {
String s = x.toUpperCase();
System.out.println(s);
});
// 形式3:多个参数
Map<String, Integer> map = new HashMap<>();
map.forEach((k, v) -> System.out.println(k + "=" + v));
// 形式4:方法引用
list.forEach(System.out::println);
// 形式5:构造方法引用
List<String> names = Arrays.asList("A", "B");
List<Person> people = names.stream()
.map(Person::new) // 构造方法引用
.collect(Collectors.toList());
1.2 Lambda 的类型推断
Lambda 表达式的类型是由上下文推断的:
// 这里 x -> x * 2 被推断为 Function<Integer, Integer>
Function<Integer, Integer> func = x -> x * 2;
// 这里的 x -> x * 2 被推断为 IntUnaryOperator
IntUnaryOperator op = x -> x * 2;
// 同一个 Lambda 在不同上下文有不同的类型!
这种机制叫做目标类型推断(Target Typing)。
二、Lambda 的编译转换
2.1 匿名内部类 vs Lambda
先看看匿名内部类编译后是什么:
// 匿名内部类
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
// 编译后会生成:RunnableImpl.class
Lambda 编译后不会生成单独的 .class 文件,而是使用 invokedynamic 指令。
2.2 用 javap 查看字节码
写一个简单的测试:
public class LambdaTest {
public static void main(String[] args) {
Runnable r = () -> System.out.println("Hello");
r.run();
}
}
编译后查看字节码:
javac LambdaTest.java
javap -c -p LambdaTest.class
public class LambdaTest {
// 不需要实例字段存储 Lambda
public static void main(java.lang.String[]);
Code:
0: invokedynamic #2, 0 // BootstrapMethods
5: astore_1
6: aload_1
7: invokeinterface #3, 1
12: return
// 引导方法(Bootstrap Method)
private static void lambda$0();
Code:
0: getstatic #7
3: ldc #9
5: invokevirtual #10
8: return
}
注意:invokedynamic #2, 0 没有生成单独的类文件!
2.3 invokedynamic 是什么
invokedynamic 是 JVM 引入的第五条方法调用指令(其他四条是 invokevirtual、invokespecial、invokestatic、invokeinterface)。
invokedynamic 的特点是:方法的链接延迟到运行时。
// 普通方法调用
invokevirtual #methodRef // 编译时就决定了调用哪个方法
// invokedynamic
invokedynamic #bootstrap, #name // 运行时才决定调用哪个方法
3.1 Bootstrap Method
invokedynamic 在第一次执行时会调用引导方法(Bootstrap Method):
// javap 输出
0: invokedynamic #2, 0 // #2 是常量池索引
// 常量池内容
#2 = InvokeDynamic #0:#30 // #name_and_type = run()V
// #bootstrap = LambdaMetafactory.metafactory
public static CallSite metafactory(
MethodHandles.Lookup caller, // 调用者
String invokedName, // 方法名(run)
MethodType invokedType, // 期望的类型(() -> Runnable)
MethodType samMethodType, // 抽象方法类型(() -> void)
MethodHandle implMethod, // 实现方法
MethodType instantiatedMethodType // 实例化方法类型
) {
// 创建 CallSite,返回一个 CallSite
return new ConstantCallSite(
MethodHandles.Lookup.IMPL_LOOKUP
.findStatic(
LambdaMetafactory.class,
"lambda$0", // Lambda 的实现方法名
invokedType
)
);
}
3.3 Lambda 的实现原理图
第一次调用 invokedynamic
│
▼
LambdaMetafactory.metafactory()
│
▼
在堆上生成 Lambda 实现类的子类
│
▼
┌─────────────┐
│ 匿名类 │
│ Runnable$1 │
└─────────────┘
3.4 生成的实现类
Lambda 的实现类是运行时在堆上生成的,JVM 不会把它写成 .class 文件:
// Lambda: () -> System.out.println("Hello")
// 生成的实现类类似于:
final class LambdaTest$$Lambda$1 implements Runnable {
// 捕获的变量(如果有)
public void run() {
// Lambda 的方法体
System.out.println("Hello");
}
}
四、Lambda 的捕获机制
4.1 非捕获 Lambda
不引用外部变量的 Lambda:
Runnable r = () -> System.out.println("Hello");
编译后很简单,不需要额外字段。
4.2 捕获 Lambda
引用外部变量的 Lambda:
String msg = "Hello";
Runnable r = () -> System.out.println(msg);
编译后会生成一个持有外部变量的实现类:
// 类似于生成这样的类:
final class LambdaTest$$Lambda$2 implements Runnable {
private final String msg; // 捕获的变量
LambdaTest$$Lambda$2(String msg) {
this.msg = msg;
}
public void run() {
System.out.println(msg);
}
}
4.3 ❌ Lambda 捕获的常见错误
错误1:修改捕获的变量
int n = 0;
Runnable r = () -> System.out.println(n++); // ❌ 编译错误!
// Lambda 只能捕获 final 或 effectively final 的变量
// n++ 修改了 n,所以不能捕获
错误2:在 Lambda 内部修改外部变量
List<Integer> list = new ArrayList<>();
list.forEach(x -> {
// ❌ 这是可以的,但有坑
list.remove(x); // 不要在遍历时修改集合!
});
4.4 【直观类比】捕获机制
【直观类比】
Lambda 的捕获就像"拍照":
- 非捕获 Lambda:不拍任何照片,直接执行
- 捕获 Lambda:先把外部变量的值"拍下来"(快照),执行时用快照的值
如果变量在拍照后被修改,Lambda 看不到新值,因为它用的是快照。
五、方法引用详解
5.1 四种方法引用
5.2 静态方法引用
// Lambda
Function<Integer, String> lambda = x -> String.valueOf(x);
// 方法引用
Function<Integer, String> ref = String::valueOf;
// 底层生成的 Lambda 类
final class ... implements Function<Integer, String> {
public String apply(Integer x) {
return String.valueOf(x);
}
}
5.3 实例方法引用(特定对象)
String prefix = "Hello ";
Function<String, String> lambda = x -> prefix.concat(x);
// 方法引用
Function<String, String> ref = prefix::concat;
// 底层生成的 Lambda 类
final class ... implements Function<String, String> {
private final String prefix;
public String apply(String x) {
return prefix.concat(x);
}
}
5.4 实例方法引用(任意对象)
// Lambda
Function<String, String> lambda = x -> x.toUpperCase();
// 方法引用
Function<String, String> ref = String::toUpperCase;
// 底层:第一个参数作为调用者
// 相当于 (x, instance) -> instance.method(x)
5.5 构造方法引用
// Lambda
Supplier<ArrayList<String>> lambda = () -> new ArrayList<>();
// 构造方法引用
Supplier<ArrayList<String>> ref = ArrayList::new;
// 用于 Stream
List<String> names = Arrays.asList("a", "b");
List<ArrayList<String>> lists = names.stream()
.map(ArrayList::new) // 调用 new ArrayList<>(String)
.collect(Collectors.toList());
六、反编译看 Lambda 实现
6.1 使用 CFR 反编译器
# 安装 CFR 或使用在线工具
java -jar cfr_0_150.jar LambdaTest.class --decodelambda
6.2 生成的代码示例
输入代码:
public class LambdaTest {
public static void main(String[] args) {
Function<String, Integer> f = Integer::parseInt;
System.out.println(f.apply("123"));
}
}
CFR 反编译后的结果:
// 生成的 Lambda 类
final class LambdaTest$$Lambda$1 implements Function {
private static final LambdaTest$$Lambda$1 INSTANCE;
static {
INSTANCE = new LambdaTest$$Lambda$1();
}
public final String apply(Object obj) {
return Integer.toString(((Number) obj).intValue());
}
public static Function get$Lambda() {
return INSTANCE; // 单例!
}
}
注意:非捕获的 Lambda 通常是单例。
6.3 捕获 Lambda 的生成代码
输入代码:
public class LambdaTest {
public static void main(String[] args) {
String suffix = "!";
Runnable r = () -> System.out.println("Hello" + suffix);
r.run();
}
}
CFR 反编译后的结果:
final class LambdaTest$$Lambda$1 implements Runnable {
private final String arg$1;
private LambdaTest$$Lambda$1(String arg$1) {
this.arg$1 = arg$1;
}
private static Runnable get$Lambda(String arg$1) {
return new LambdaTest$$Lambda$1(arg$1);
}
public final void run() {
System.out.println("Hello" + this.arg$1);
}
}
七、Lambda 与匿名内部类的对比
7.1 性能对比
7.2 this 指向对比
public class LambdaTest {
public Runnable getRunnable(String name) {
// 匿名内部类:this 指向匿名内部类
return new Runnable() {
@Override
public void run() {
System.out.println(this.getClass()); // Runnable$1
}
};
}
public Runnable getLambda(String name) {
// Lambda:this 指向外围类
return () -> {
System.out.println(this.getClass()); // LambdaTest
};
}
}
💡
Lambda 的 this 指向的是定义 Lambda 的那个类的实例,而不是 Lambda 本身。这是因为 Lambda 不会生成自己的类,它的方法是"静态"的(或者是合成的实例方法)。
八、生产避坑
8.1 ❌ 错误示范:Lambda 中捕获可变对象
List<String> list = new ArrayList<>();
Consumer<String> consumer = s -> list.add(s); // ❌ 捕获了可变对象
// 可能导致的问题:
// 1. 并发问题:多个线程共享 list
// 2. 内存泄漏:list 被 lambda 持有,生命周期延长
正确做法:
// 方式1:不要捕获可变对象
List<String> result = new ArrayList<>();
list.forEach(s -> {
List<String> local = new ArrayList<>(); // 用局部变量
local.add(s);
});
// 方式2:使用 stream 的 collectors
List<String> result = list.stream()
.collect(Collectors.toList());
8.2 ❌ 错误示范:在循环中创建大量 Lambda
for (int i = 0; i < 10000; i++) {
// 每次循环都创建新的 Lambda 对象
callbacks.add(() -> handle(i)); // 而且 i 还是错的
}
正确做法:
// 用 final 变量捕获
for (int i = 0; i < 10000; i++) {
final int index = i; // effectively final
callbacks.add(() -> handle(index));
}
8.3 ❌ 错误示范:Lambda 链路太长难调试
// 链路过长的 Lambda
list.stream()
.filter(x -> x > 0)
.map(x -> transform(x))
.map(x -> anotherTransform(x))
.collect(Collectors.toList());
// 如果出问题很难定位在哪一步
正确做法:适当拆分或增加日志:
List<Integer> result = list.stream()
.filter(x -> x > 0)
.peek(x -> System.out.println("After filter: " + x)) // 用 peek 调试
.map(x -> transform(x))
.collect(Collectors.toList());
九、面试追问链
第一层:基础概念
面试官问:"Lambda 表达式编译后变成什么?"
Lambda 不会生成单独的 .class 文件,而是使用 invokedynamic 指令。运行时通过 LambdaMetafactory 生成实现类。
第二层:invokedynamic
面试官追问:"invokedynamic 是什么?和普通方法调用有什么区别?"
invokedynamic 是 JVM 的第五条方法调用指令,普通方法调用在编译时就决定了目标方法,而 invokedynamic 的链接延迟到运行时,第一次调用时才调用引导方法创建 CallSite。
第三层:Lambda 捕获
面试官追问:"Lambda 可以捕获什么?"
Lambda 只能捕获 final 或 effectively final 的变量。被捕获的变量会在生成的 Lambda 类中作为字段存储。
第四层:性能优化
面试官追问:"Lambda 和匿名内部类哪个性能更好?"
Lambda 性能更好:不需要生成单独的 .class 文件,非捕获 Lambda 可能是单例,invokedynamic 惰性创建。第一次执行略慢(要调用引导方法),之后调用和普通方法一样快。
【学习小结】
- Lambda 使用 invokedynamic 指令,运行时生成实现类
- LambdaMetafactory.metafactory 是引导方法
- Lambda 可以捕获 final 或 effectively final 的变量
- 非捕获 Lambda 通常是单例
- Lambda 的 this 指向定义它的类,不是 Lambda 自身
- Lambda 性能优于匿名内部类