Lambda表达式的用法和原理

深入学习Java基础知识
2021-05-17 14:29 · 阅读时长7分钟
小课

Lambda表达式是Java 8发布的新特性,主要是用于简化匿名内部类作为参数传递的写法,省去了一些模版代码,让代码更加简洁,Lambda表达式有以下几个特点

  • 没有冲突的情况下,无需声明参数类型。
  • 一个参数是可以不用()将参数括起来,没有参数或者多个参数需要。
  • 主体只有一条语句,可以不用{},多条语句需要。
  • 主体只有一个表达式语句,可以不用写return关键字,自动返回表达式值。

虽然Lambda表达式和匿名内部类关系密切,但实际上它和匿名内部类还是不同的,比如

  • 在匿名内部类中,this指代的是匿名内部类,而Lambda表达式指代的是当前调用的类。
  • 匿名内部类在编译后会生成一个单独的字节码文件,而Lambda表达式会在调用的类中新增一个私有静态方法,这点也说明Lambda表达式并不是在编译时直接转化为匿名内部类实现的。

我们知道Java中只有两种类型,基本类型和引用类型,Lambda表达式很明显不是基本类型,那么Lambda表达式是如何转化为引用类型的呢?下面以一个简单的示例,看看Lambda表达式最终是如何转化并实现调用的。

文件大小修改时间
Main.java
295 B 2021年08月18日
字节码.txt
1009 B 2021年08月18日
字节码完整版.txt
6 kB 2021年08月18日

查看字节码.txt文件27行,可以看出多了一个方法lambda$main$0(int, int),它的参数列表类型和返回类型和Lambda表达式是一致的,可以推断Lambda表达式编译成了一个私有静态方法,虽然编译成了一个私有静态方法,但是字节码中并没有地方直接调用它,原本调用Lambda表达式的地方只是调用了invokedynamic指令,所以猜测invokedynamic指令最终调用了lambda$main$0(int, int)

invokedynamic是Java字节码中的一个方法调用指令,它和其它方法调用指令不同的是它调用的方法并不是在编译时确定好的,而是通过一个叫bootstrap method的方法来获取的,而bootstrap method方法具体返回什么是由我们自己实现的。Lambda表达式对应的bootstrap method定义在java.lang.invoke.LambdaMetafactory.metafactory(...),本示例的invokedynamic解析执行流程大致如下

字节码完整版.txt第86行,执行invokedynamic指令

0: invokedynamic            #2,  0        #2表示调用信息在常量池的索引

字节码完整版.txt第11行,在常量池#2获取动态调用需要的参数

#2 = InvokeDynamic      #0:#29     #0表示在BootstrapMethods的索引,#29表示方法和类型在常量池中的索引

字节码完整版.txt第38,50,51行,在常量池中找到类型和方法

#29 = NameAndType    #41:#42
#41 = Utf8                      call
#42 = Utf8                      ()LMain$FunInterface;

字节码完整版.txt第117行,在BootstrapMethods#0里面找到对应的bootstrap method

0: #26 invokestatic java/lang/invoke/LambdaMetafactory.metafactory(...)...
    Method arguments:
      #27 (II)I
      #28 invokestatic Main.lambda$main$0:(II)I
      #27 (II)I

拿到动态调用所有需要的信息后,虚拟机会调用bootstrap method,也就是LambdaMetafactory.metafactory(...),在这个方法里面会生成一个实现FunInterface接口的内部类,并且在这个类的call方法中调用Lambda表达式所生成的私有静态方法lambda$main$0,最后返回一个CallSite对象,而这个CallSite对象会包含一个MethodHandle,通过调用这个MethodHandle,就可以实例化一个刚才生成的内部类。这样invokedynamic执行完成后就会返回一个实现FunInterface,并且在call方法里面调用Lambda表达式生成的私有静态方法的内部类。

简单来说,invokedynamic指令通过参数找到并调用了LambdaMetafactory.metafactory(...)方法,在运行时创建了一个实现FunInterface接口的内部类,然后通过这个内部类最终调用了lambda$main$0方法。

lambdajava8java