万书网 > 文学作品 > Java实战(第2版) > 附录 D Lambda表达式和JVM字节码

附录 D Lambda表达式和JVM字节码

    你可能会好奇Java编译器是如何实现Lambda表达式,而Java虚拟机又是如何对它们进行处理的。如果你认为Lambda表达式就是简单地被转换为匿名类,那就太天真了,请继续阅读下去。本附录通过审视编译生成的.class文件,简要地讨论Java是如何编译Lambda表达式的。

    D.1 匿名类

    第2章已经介绍过,匿名类可以同时声明和实例化一个类。因此,它们和Lambda表达式一样,也能用于提供函数式接口的实现。

    由于Lambda表达式提供了函数式接口中抽象方法的实现,这让人有一种感觉,似乎在编译过程中让Java编译器直接将Lambda表达式转换为匿名类更直观。不过,匿名类有着种种不尽如人意的特性,会给应用程序的性能带来负面影响。

    编译器会为每个匿名类生成一个新的.class文件。这些新生成的类文件的文件名通常以ClassName$1这种形式呈现,其中ClassName是匿名类出现的类的名字,紧跟着一个美元符号和一个数字。生成大量的类文件是不利的,因为每个类文件在使用之前都需要加载和验证,这会直接影响应用的启动性能。如果将Lambda表达式转换为匿名类,那么每个Lambda表达式都会产生一个新的类文件,这是我们不期望发生的。

    每个新的匿名类都会为类或者接口产生一个新的子类型。如果你为了实现一个比较器,使用了一百多个不同的Lambda表达式,这意味着该比较器会有一百多个不同的子类型。这种情况下,JVM的运行时性能调优会变得更加困难。

    D.2 生成字节码

    Java的源代码文件会经由Java编译器编译为Java字节码。之后JVM可以执行这些生成的字节码运行应用。编译时,匿名类和Lambda表达式使用了不同的字节码指令。你可以通过下面这条命令查看任何类文件的字节码和常量池:

    javap -c -v ClassName

    我们试着使用Java 7中旧的格式实现了Function接口的一个实例,代码如下所示。

    代码清单 D-1 以匿名内部类的方式实现的一个Function接口

    import java.util.function.Function; public class InnerClass { Function f = new Function() { @Override public String apply(Object obj) { return obj.toString(); } }; }

    这种方式下,和Function对应,以匿名内部类形式生成的字节码看起来就像下面这样:

    0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: aload_0 5: new #2 // class InnerClass$1 8: dup 9: aload_0 10: invokespecial #3 // Method InnerClass$1."":(LInnerClass;)V 13: putfield #4 // Field f:Ljava/util/function/Function; 16: return

    这段代码展示了下面这些编译中的细节。

    通过字节码操作new,一个InnerClass$1类型的对象被实例化了。与此同时,一个指向新创建对象的引用会被压入栈。

    dup操作会复制栈上的引用。

    接着,这个值会被invokespecial指令处理,该指令会初始化对象。

    栈顶现在包含了指向对象的引用,该值通过putfield指令保存到了LambdaBytecode类的f1字段。

    InnerClass$1是由编译器为匿名类生成的名字。如果你想要再次确认这一情况,也可以查看InnerClass$1类文件,你可以看到Function接口的实现代码如下:

    class InnerClass$1 implements java.util.function.Function { final InnerClass this$0; public java.lang.String apply(java.lang.Object); Code: 0: aload_1 1: invokevirtual #3 // Method java/lang/Object.toString:()Ljava/lang/String; 4: areturn }

    D.3 用InvokeDynamic力挽狂澜

    现在,试着采用Java 8中新提供的Lambda表达式来完成同样的功能。我们会查看下面这段代码清单生成的类文件。

    代码清单 D-2 使用Lambda表达式实现的Function

    import java.util.function.Function; public class Lambda { Function f = obj -> obj.toString(); }

    你会看到下面这些字节码指令:

    0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: aload_0 5: invokedynamic #2, 0 // InvokeDynamic #0:apply:()Ljava/util/function/Function; 10: putfield #3 // Field f:Ljava/util/function/Function; 13: return

    我们已经解释过将Lambda表达式转换为内部匿名类的缺点,通过这段字节码你可以再次确认二者之间巨大的差别。创建额外的类现在被invokedynamic指令替代了。

    invokedynamic指令

    字节码指令invokedynamic最初被JDK7引入,用于支持运行于JVM上的动态类型语言。执行方法调用时,invokedynamic添加了更高层的抽象,使得一部分逻辑可以依据动态语言的特征来决定调用目标。这一指令的典型使用场景如下:

    def add(a, b) { a + b }

    这里a和b的类型在编译时都未知,有可能随着运行时发生变化。由于这个原因,JVM首次执行invokedynamic调用时,它会查询一个bootstrap方法,该方法实现了依赖语言的逻辑,可以决定选择哪一个方法进行调用。bootstrap方法返回一个链接调用点(linked call site)。很多情况下,如果add方法使用两个int类型的变量,那么紧接下来的调用也会使用两个int类型的值。所以,每次调用也没有必要都重新选择调用的方法。调用点自身就包含了一定的逻辑,可以判断在什么情况下需要进行重新链接。

    代码清单D-2中,使用invokedynamic指令的目的略微有别于我们最初介绍的那一种。这个例子中,它被用于延迟Lambda表达式到字节码的转换,最终这一操作被推迟到了运行时。换句话说,以这种方式使用invokedynamic,可以将实现Lambda表达式的这部分代码的字节码生成推迟到运行时。这种设计选择带来了一系列好结果。

    Lambda表达式的代码块到字节码的转换由高层的策略变成了纯粹的实现细节。它现在可以动态地改变,或者在未来版本中得到优化、修改,并且保持了字节码的后向兼容性。

    没有带来额外的开销,没有额外的字段,也不需要进行静态初始化,而这些如果不使用Lambda,就不会实现。

    对无状态非捕获型Lambda,可以创建一个Lambda对象的实例,对其进行缓存,之后对同一对象的访问都返回同样的内容。这是一种常见的用例,也是人们在Java 8之前就惯用的方式,比如,以static final变量的方式声明某个比较器实例。

    没有额外的性能开销,因为这些转换都是必须的,并且结果也进行了链接,仅在Lambda首次被调用时需要转换,其后所有的调用都能直接跳过这一步,直接调用之前链接的实现。

    D.4 代码生成策略

    将Lambda表达式的代码体填入到运行时动态创建的静态方法,就完成了Lambda表达式的字节码转换。无状态Lambda在它涵盖的范围内不保持任何状态信息,就像在代码清单D-2中定义的那样,字节码转换时它是所有Lambda中最简单的一种类型。这种情况下,编译器可以生成一个方法,此方法含有该Lambda表达式同样的签名,所以最终转换的结果从逻辑上看起来就像下面这样:

    public class Lambda { Function f = [dynamic invocation of lambda$1] static String lambda$1(Object obj) { return obj.toString(); } }

    Lambda表达式中包含了final(或者效果上等同于final)的本地变量或者字段的情况会稍微复杂一些,就像下面的这个例子:

    public class Lambda { String header = "This is a "; Function f = obj -> header + obj.toString(); }

    这个例子中,生成方法的签名不会和Lambda表达式一样,因为它还需要携带参数来传递上下文中额外的状态。为了实现这一目标,最简单的方案是在Lambda表达式中为每一个需要额外保存的变量预留参数,所以实现前面Lambda表达式的生成方法会像下面这样:

    public class Lambda { String header = "This is a "; Function f = [dynamic invocation of lambda$1] static String lambda$1(String header, Object obj) { return obj -> header + obj.toString(); } }

    更多关于Lambda表达式转换流程的内容,可以访问如下地址:http://cr.openjdk.java/~briangoetz/lambda/lambda-translation.html。