java8 lambda表达式原理

时间:2021-10-11 19:04:44

java8 lambda表达式原理

java8已经推出有一段时间了,相信有不少公司已经把jdk升级到8了,每次jdk的升级都会带来一些性能以及应用上的优化,比如8移出了永久区,java.lang.OutOfMemoryError: PermGen space离我们而去,以及一些新的语法糖lambda,stream,默认方法等等,本文就来说说lambda表达式

lambda表达式写法

基本语法:
(parameters) -> expression

(parameters) ->{ statements; }

左边为传入参数,右边为执行代码,代表一个函数式接口的实现

lambda例子

此处不多写,贴出其他人的博客,请见Java8 lambda表达式10个示例

lambda表达式与旧的api对比

  • 对比 code
public class LambdaTest {

    public static void main(String[] args) {
        ArrayList<Integer> integers = new ArrayList<Integer>() ;
        integers.add(9);
        integers.add(8);
        integers.add(7);
        integers.add(6);
        integers.sort(new Comparator<Integer>() {
            public int compare(Integer o1, Integer o2) {
                return o1.compareTo(o2);
            }
        });
        System.out.println("匿名内部类排序输出:"+integers);


        integers.sort((o1,o2)->o1.compareTo(02));
        System.out.println("lambda1表达式排序输出:"+integers);


        integers.sort(Integer::compareTo);
        System.out.println("lambda2表达式排序输出:"+integers);
    }
}
  • 对比 输出
匿名内部类排序输出:[6, 7, 8, 9]
lambda1表达式排序输出:[6, 7, 8, 9]
lambda2表达式排序输出:[6, 7, 8, 9]
  • 编译后的文件
    java8 lambda表达式原理
    从上面看到,生成了两个class文件,一个是LambdaTest类的class文件,一个是Comparator匿名内部类的class文件,lambda表达式并未生成匿名内部类class文件,也就是说java8并不是靠编译器将lambda转换为匿名内部类,lambda脱糖过程稍后描述。

lambda原理

在java8中每一个Lambda表达式必须有一个函数式接口与之对应,那么函数式接口是什么呢?

什么是函数式接口

函数式接口(functional interface)简单来说就是只包含一个抽象方法的普通接口,java.lang.Runnable、java.util.Comparator都是函数式接口,java8提供了java.lang.FunctionalInterface注解进行标准,但是是非必须的,jdk会自动识别函数式接口。函数式接口可以被隐式转换为lambda表达式。

函数式与lambda表达式关系实例

  • 正确实例
public class LambdaTest2 {

    public interface TestInterface{
        public void test1();
    }

    public static void doSomething(TestInterface test){
        test.test1();
    }
    public static void main(String[] args) {
        doSomething(()-> System.out.println("HelloWorld"));
    }
}

输出HelloWorld

  • 错误实例
public class LambdaTest3 {

    public interface TestInterface{
        public void test1();
        public void test2(int a);
    }

    public static void doSomething(TestInterface test){
        test.test1();
    }
    public static void main(String[] args) {
        doSomething(()-> System.out.println("HelloWorld"));
    }
}

编译错误:
Error:(19, 21) java: 不兼容的类型: com.chen.LambdaTest3.TestInterface 不是函数接口
在 接口 com.chen.LambdaTest3.TestInterface 中找到多个非覆盖抽象方法

那么lambda是怎么跟函数式接口对应的呢

来看一下LambdaTest2的字节码

Classfile /F:/workspace/java8-test/target/classes/com/chen/LambdaTest2.class
  Last modified 2017-2-18; size 1376 bytes
  MD5 checksum 5de06d632caf9ead069b79cf854411b9
  Compiled from "LambdaTest2.java"
public class com.chen.LambdaTest2
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#31         // java/lang/Object."<init>":()V
   #2 = InterfaceMethodref #10.#32        // com/chen/LambdaTest2$TestInterface.test1:()V
   #3 = InvokeDynamic      #0:#37         // #0:test1:()Lcom/chen/LambdaTest2$TestInterface;
   #4 = Methodref          #8.#38         // com/chen/LambdaTest2.doSomething:(Lcom/chen/LambdaTest2$TestInterface;)V
   #5 = Fieldref           #39.#40        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = String             #41            // HelloWorld
   #7 = Methodref          #42.#43        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #8 = Class              #44            // com/chen/LambdaTest2
   #9 = Class              #45            // java/lang/Object
  #10 = Class              #46            // com/chen/LambdaTest2$TestInterface
  #11 = Utf8               TestInterface
  #12 = Utf8               InnerClasses
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               LocalVariableTable
  #18 = Utf8               this
  #19 = Utf8               Lcom/chen/LambdaTest2;
  #20 = Utf8               doSomething
  #21 = Utf8               (Lcom/chen/LambdaTest2$TestInterface;)V
  #22 = Utf8               test
  #23 = Utf8               Lcom/chen/LambdaTest2$TestInterface;
  #24 = Utf8               main
  #25 = Utf8               ([Ljava/lang/String;)V
  #26 = Utf8               args
  #27 = Utf8               [Ljava/lang/String;
  #28 = Utf8               lambda$main$0
  #29 = Utf8               SourceFile
  #30 = Utf8               LambdaTest2.java
  #31 = NameAndType        #13:#14        // "<init>":()V
  #32 = NameAndType        #47:#14        // test1:()V
  #33 = Utf8               BootstrapMethods
  #34 = MethodHandle       #6:#48         // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #35 = MethodType         #14            // ()V
  #36 = MethodHandle       #6:#49         // invokestatic com/chen/LambdaTest2.lambda$main$0:()V
  #37 = NameAndType        #47:#50        // test1:()Lcom/chen/LambdaTest2$TestInterface;
  #38 = NameAndType        #20:#21        // doSomething:(Lcom/chen/LambdaTest2$TestInterface;)V
  #39 = Class              #51            // java/lang/System
  #40 = NameAndType        #52:#53        // out:Ljava/io/PrintStream;
  #41 = Utf8               HelloWorld
  #42 = Class              #54            // java/io/PrintStream
  #43 = NameAndType        #55:#56        // println:(Ljava/lang/String;)V
  #44 = Utf8               com/chen/LambdaTest2
  #45 = Utf8               java/lang/Object
  #46 = Utf8               com/chen/LambdaTest2$TestInterface
  #47 = Utf8               test1
  #48 = Methodref          #57.#58        // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #49 = Methodref          #8.#59         // com/chen/LambdaTest2.lambda$main$0:()V
  #50 = Utf8               ()Lcom/chen/LambdaTest2$TestInterface;
  #51 = Utf8               java/lang/System
  #52 = Utf8               out
  #53 = Utf8               Ljava/io/PrintStream;
  #54 = Utf8               java/io/PrintStream
  #55 = Utf8               println
  #56 = Utf8               (Ljava/lang/String;)V
  #57 = Class              #60            // java/lang/invoke/LambdaMetafactory
  #58 = NameAndType        #61:#64        // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #59 = NameAndType        #28:#14        // lambda$main$0:()V
  #60 = Utf8               java/lang/invoke/LambdaMetafactory
  #61 = Utf8               metafactory
  #62 = Class              #66            // java/lang/invoke/MethodHandles$Lookup
  #63 = Utf8               Lookup
  #64 = Utf8               (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #65 = Class              #67            // java/lang/invoke/MethodHandles
  #66 = Utf8               java/lang/invoke/MethodHandles$Lookup
  #67 = Utf8               java/lang/invoke/MethodHandles
{
  public com.chen.LambdaTest2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/chen/LambdaTest2;

  public static void doSomething(com.chen.LambdaTest2$TestInterface);
    descriptor: (Lcom/chen/LambdaTest2$TestInterface;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokeinterface #2,  1            // InterfaceMethod com/chen/LambdaTest2$TestInterface.test1:()V
         6: return
      LineNumberTable:
        line 15: 0
        line 16: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  test   Lcom/chen/LambdaTest2$TestInterface;

  public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: invokedynamic #3,  0              // InvokeDynamic #0:test1:()Lcom/chen/LambdaTest2$TestInterface;
         5: invokestatic  #4                  // Method doSomething:(Lcom/chen/LambdaTest2$TestInterface;)V
         8: return
      LineNumberTable:
        line 18: 0
        line 19: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "LambdaTest2.java"
InnerClasses:
     public static #11= #10 of #8; //TestInterface=class com/chen/LambdaTest2$TestInterface of class com/chen/LambdaTest2
     public static final #63= #62 of #65; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #34 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #35 ()V
      #36 invokestatic com/chen/LambdaTest2.lambda$main$0:()V
      #35 ()V

其中main方法

 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: invokedynamic #3,  0              // InvokeDynamic #0:test1:()Lcom/chen/LambdaTest2$TestInterface;
         5: invokestatic  #4                  // Method doSomething:(Lcom/chen/LambdaTest2$TestInterface;)V
         8: return
      LineNumberTable:
        line 18: 0
        line 19: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;

执行了三条指令 invokedynamic、invokestatic、return

  • 第一条是lambda表达式转化为函数式接口TestInterface
  • 第二条执行doSomething方法
  • 第三条退出main方法

invokedynamic指令是在jvm7中新增的,invokedynamic出现的位置代表一个动态调用点
invokedynamic指令后面会跟一个指向常量池的调用点限定符,这个限定符会被解析为一个动态调用点。
调用点限定符的符号引用为CONSTANT_InvokeDynamic_info结构

CONSTANT_InvokeDynamic_info{  
    u1 tag;  
    u2 bootstrap_method_attr_index;   
    u2 name_and_type_index;  
}  

依据这个可以找到对应的动态调用引导方法Java.lang.invoke.CallSite

此处invokedynamic后面跟的是常量#3,#3指向#0和#37,#37代表TestInterface接口的test1方法

#3 = InvokeDynamic      #0:#37         // #0:test1:()Lcom/chen/LambdaTest2$TestInterface;
#37 = NameAndType        #47:#50        // test1:()Lcom/chen/LambdaTest2$TestInterface;

而#0在字节码最后的BootstrapMethods中,Method arguments#36代表这个lambda表达式调用代码

BootstrapMethods:
  0: #34 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #35 ()V
      #36 invokestatic com/chen/LambdaTest2.lambda$main$0:()V
      #35 ()V

根据BootstrapMethods对应的#34可以找到此处lambda InvokeDynamic指令对应的引导方法是LambdaMetafactory.metafactory,其返还一个CallSite

    public static CallSite metafactory(MethodHandles.Lookup caller,
                                       String invokedName,
                                       MethodType invokedType,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType)
            throws LambdaConversionException {
        AbstractValidatingLambdaMetafactory mf;
        mf = new InnerClassLambdaMetafactory(caller, invokedType,
                                             invokedName, samMethodType,
                                             implMethod, instantiatedMethodType,
                                             false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
        mf.validateMetafactoryArgs();
        return mf.buildCallSite();
    }

观察源码可得,其通过new一个InnerClassLambdaMetafactory并调用buildCallSite方法创造了类似内部类的lambda CallSite
有兴趣可以看buildCallSite方法的源码
InnerClassLambdaMetafactory类的源码注释是

Lambda metafactory implementation which dynamically creates an inner-class-like class per lambda callsite.

至此,lambda表达式的脱糖过程已经了解完成,其中很多细节设计太多java字节码知识,本汪也不是太了解,有兴趣可以多看看java虚拟机规范

至于为什么要绕一圈生成一个内部类的动态调用点然后执行,而不是直接把lambda编译成内部类,也许是为了减少编译后的文件数,具体不得而知,有待研究