Java Lambda 底层实现

lambda不像匿名内部类生成一大堆的.class文件,他的底层实现原理主要如下,分为编译,运行,首次调用等过程

例如有这样一段代码:

java 复制代码
Runnable task = () -> System.out.println("hello");
task.run();

在编译阶段,javac 首先会把 Lambda 表达式中的方法体抽取出来,放到当前类中,生成一个合成方法:

java 复制代码
//static类主要用于后续适配器的调用
private static void lambda$main$0() {
    System.out.println("hello");
}

所以 Lambda 中真正的业务代码,在编译期就已经被编译成普通字节码,并保存在当前类的 .class 文件中了。

但是程序还需要得到一个 Runnable 对象,否则后面的 task.run() 无法执行。对于匿名内部类,编译器会直接生成一个 LambdaDemo$1.class,然后在代码中执行 new LambdaDemo$1()。Lambda 没有这么做,它会把创建 Runnable 对象的位置编译成一条 invokedynamic 指令。

arduino 复制代码
invokedynamic run:()Ljava/lang/Runnable;

这个字节码的作用不是直接执行 Lambda 方法体,而是负责在程序第一次运行到这里时,确定这个位置以后怎样创建 Runnable 对象。

除了生成 invokedynamic 指令,javac 还会在当前类的 BootstrapMethods 属性中记录一份 Lambda 的描述信息。这里面会说明目标函数式接口是 Runnable,接口方法是 run(),真正的实现方法是 LambdaDemo.lambda$main$0(),引导方法则是 LambdaMetafactory.metafactory() 。当 JVM 第一次执行到这条 invokedynamic 指令时就会调用这个引导方法,并把 Runnable、run() 和 lambda$main$0() 等信息传给它。

LambdaMetafactory 的任务,就是为这个 invokedynamic 调用点构造一个 CallSite。这个 CallSite 可以简单理解为:以后程序再执行到这个位置时,具体应该调用哪段逻辑来得到 Lambda 对象。

在 JDK 8 的具体实现中,LambdaMetafactory 最终通过 ASM 在内存中动态生成一个类,这个类的逻辑大致相当于:

java 复制代码
final class LambdaDemo$$Lambda$1 implements Runnable {

    @Override
    public void run() {
        LambdaDemo.lambda$main$0();
    }
}

这个类本身并不包含真正的业务逻辑,它主要是一个适配器。它负责实现 Runnable 接口,并把 run() 方法转发到编译期已经生成的 lambda$main$0() 方法。

这个class文件不会和匿名内部类一样写入磁盘而是 InnerClassLambdaMetafactory 在程序运行过程中,通过 ASM 生成一个 byte[]。这个字节数组的内容符合 class 文件格式,但它只存在于内存中,不会默认写成 LambdaDemo$$Lambda$1.class 文件。

当动态类生成完毕以后,LambdaMetafactory 会返回一个已经绑定好的 CallSite。以后再次执行同一个 invokedynamic 调用点时,就不需要重新调用 LambdaMetafactory,也不需要重新生成实现类,而是直接使用已经链接好的 CallSite 创建或者返回 Lambda 对象。

对于没有捕获外部变量的 Lambda,生成的实现类不需要保存任何字段,因此运行时通常可以创建一个对象并重复使用。此时执行 invokedynamic,通常就是返回这个已经创建好的 Runnable 实例。

**lambda如何捕获外部变量?**实际上就是通过参数传递进去,如下,对于捕获了外部变量的 Lambda:

java 复制代码
int value = 10;
Runnable task = () -> System.out.println(value);

编译器会把 Lambda 方法体转换成类似下面的方法:

java 复制代码
private static void lambda$main$0(int value) {
    System.out.println(value);
}

当执行到invokedynamic时候会把外部变量复制一份

运行时生成的实现类则需要增加字段,用来保存捕获到的 value:

java 复制代码
final class LambdaDemo$$Lambda$1 implements Runnable {

    private final int arg$1;

    LambdaDemo$$Lambda$1(int value) {
        this.arg$1 = value;
    }

    @Override
    public void run() {
        LambdaDemo.lambda$main$0(arg$1);
    }
}

这种情况下,Lambda 实现类通常仍然只生成一次,但每次执行 Lambda 创建表达式时,都可能创建一个新的对象实例,并把当前捕获到的值存入对象字段。

最终执行 task.run() 时,这已经是一次普通的接口方法调用。它先调用运行时生成类的 run(),然后再由这个 run() 转发到 LambdaDemo.lambda$main$0(),真正执行 Lambda 中的业务代码。代码成为热点以后,JIT 还可能把中间的接口调用、适配器方法和合成方法全部内联,最终直接生成对应的机器码。

所以整个过程可以概括为:javac 在编译期把 Lambda 方法体生成普通方法,同时在创建 Lambda 对象的位置生成 invokedynamic;第一次执行 invokedynamic 时,JVM 调用 LambdaMetafactory;LambdaMetafactory 在运行时生成一个实现函数式接口的类,并将其直接定义到 JVM 中;invokedynamic 返回这个类的对象;调用函数式接口方法时,再转发到编译期生成的 Lambda 合成方法。

相关推荐
殷色玫瑰35 分钟前
C++ STL:stack、queue、priority_queue 与容器适配器详解
java·开发语言·数据结构·c++·算法·visualstudio
IT方大同1 小时前
java输入输出+方法
java·开发语言·python
Yyyyyy~1 小时前
【Java】String
java
Wang's Blog1 小时前
Java框架 SpringCloud 快速入门: 服务拆分之服务远程调用
java·开发语言·spring cloud
Wang's Blog1 小时前
Java框架 SpringCloud 快速入门: 路由断言工厂
java·spring·spring cloud
ym hyd 1111 小时前
门诊挂号系统源码 Java+SpringBoot+Vue3 前后分离
java·vue.js·spring boot·毕设
杨丰玮4181 小时前
C语言核心语法
java·c语言·开发语言·学习方法
海宇AI1 小时前
Java数据工程:利用海宇婚恋风险报告优化高端婚恋实名与涉诉核验合规体验
java·人工智能
CEZ2 小时前
中小企业上 WMS 该先上哪几块:JeeWMS 开源 Java 仓库管理系统的分批上线清单
java·开源
Wang's Blog2 小时前
Java框架 SpringCloud 快速入门: Gateway 路由的过滤器配置
java·spring cloud·gateway