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  // 运行时才决定调用哪个方法

三、LambdaMetafactory:运行时生成实现类

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

3.2 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 四种方法引用

类型语法示例对应的 Lambda
静态方法ClassName::methodString::valueOfx -> String.valueOf(x)
实例方法(特定对象)obj::methodSystem.out::printlnx -> System.out.println(x)
实例方法(任意对象)ClassName::methodString::toUpperCasex -> x.toUpperCase()
构造方法ClassName::newArrayList::new() -> new ArrayList()

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 性能对比

维度匿名内部类Lambda
类文件数量1 个 .class 文件0 个(invokedynamic)
运行时创建每次执行都创建第一次调用时创建,之后缓存
内存开销每个实例一个类共享同一个类实例(非捕获)
启动速度较慢(要加载类)较快(invokedynamic 惰性)

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 性能优于匿名内部类