如大家所熟悉的,Java语言和JVM工程师决定将翻译策略的选择推迟到运行时。Java 7 引入的新 invokedynamic 字节码指令为他们提供了一种高效实现这一目标的机制。将 lambda 表达式翻译为字节码分两步进行: 生成一个被调用的动态调用站点(称为 lambda 工厂),该站点被调用时将返回一个功能接口实例,而 lambda 将被转换为该功能接口; 将 lambda 表达式的主体转换为一个方法,该方法将通过 invokedynamic 指令调用。 为了说明第一步,让我们检查一下编译包含 lambda 表达式的简单类时生成的字节码,例如:
import java.util.function.Function; public class Lambda { Function<String, Integer> f = s -> Integer.parseInt(s); }这将转化为以下字节码:
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
请注意,方法引用的编译方式略有不同,因为 javac 不需要生成合成方法,而是可以直接引用方法。 如何执行第二步取决于 lambda 表达式是非捕获(lambda 不访问定义在其主体之外的任何变量)还是捕获(lambda 访问定义在其主体之外的变量)。
非捕获 lambda 只需将其反调为与 lambda 表达式签名完全相同的静态方法,并在使用 lambda 表达式的同一类中声明即可。例如:在上面的 lambda 类中声明的 lambda 表达式可以这样删减为一个方法:
static Integer lambda$1(String s) { return Integer.parseInt(s); }注意:$1 并不是一个内部类,它只是我们表示编译器生成代码的一种方式。 捕获 lambda 表达式的情况要复杂一些,因为捕获的变量必须与 lambda 的形式参数一起传递给实现 lambda 表达式主体的方法。
在这种情况下,常见的翻译策略是在 lambda 表达式的参数前为每个捕获变量添加一个额外的参数。让我们来看一个实际例子:
int offset = 100; Function<String, Integer> f = s -> Integer.parseInt(s) + offset;相应方法的实现如下所示:
static Integer lambda$1(int offset, String s) { return Integer.parseInt(s) + offset; }不过,这种翻译策略并不是一成不变的,因为 invokedynamic 指令的使用为编译器提供了灵活性,使其可以在将来选择不同的实现策略。例如,可以将捕获的值装入数组,或者,如果 lambda 表达式读取了使用该表达式的类的某些字段,生成的方法可以是实例方法,而不是静态方法,这样就无需将这些字段作为额外参数传递。