D.3 用InvokeDynamic力挽狂澜

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

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

  1. import java.util.function.Function;
  2. public class Lambda {
  3. Function<Object, String> f = obj -> obj.toString();
  4. }

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

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

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

invokedynamic指令

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

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

这里ab的类型在编译时都未知,有可能随着运行时发生变化。由于这个原因,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首次被调用时需要转换,其后所有的调用都能直接跳过这一步,直接调用之前链接的实现。