首页 > 代码库 > Java8 Lambda表达式深入学习(4) -- Java8实现方式

Java8 Lambda表达式深入学习(4) -- Java8实现方式

前几篇文章讨论了函数式接口和Lambda表达式语法、invokedynamic指令,以及Groovy2如何利用indy指令。本篇文章在前面几篇的基础之上,简要介绍Java8底层是如何实现Lambda表达式的。

示例代码

本文将以下面的代码为例展开讨论:

import java.util.Arrays;
import java.util.List;

public class LambdaImplTest {
    
    public static void main(String[] args) {
        m1(Arrays.asList(args));
    }
    
    public static void m1(List<String> list) {
        list.sort((a, b) -> a.length() - b.length());
    }
    
}

插入invokedynamic指令

直接利用匿名类来实现Lambda表达式肯定也是可行的,这样,Lambda表达式就只是Java编译器的语法糖而已。但是Java8并没有这样做,而是使用了更复杂(也更灵活)的方式:利用indy指令。显然,这种方式需要编译器和JVM一同配合来实现。编译器会在每个Lambda表达式出现的地方插入一条indy指令,同时还必须在class文件里生成相应的CONSTANT_InvokeDynamic_info常量池项和BootstrapMethods属性等信息。这些信息将这条indy指令的bootstrap方法指向LambdaMetafactory.metafactory(...)方法。

插入lambda$x&y方法

javac编译LambdaImplTest.java,然后用javap -v -p反编译.class文件,可以看到,编译器生成了一个lambda$m1$0方法,并且将Lambda表达式的内容搬到了里面:

编译器会按照一定的规则来给Lambda表达式生成方法,以保证这些方法不会重名。如果把字节码还原成Java代码的话,LambdaImplTest看起来会像下面这样:

public class LambdaImplTest {
    
    public static void main(String[] args) {
        m1(Arrays.asList(args));
    }
    
    public static void m1(List<String> list) {
        list.sort(/*lambda*/);
    }
    
    private static int lambda$m1$0(String a, String b) {
        return a.length() - b.length();
    }
    
}

LambdaMetafactory.metafactory(...)方法

当JVM第一次执行到这条indy指令的时候,它会找到这条指令相应的bootstrap方法,然后调用该方法,得到一个CallSite。下面是metafactory()方法的代码:

    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();
    }
从代码可以猜到,Java8内部也是以内部类的方式来实现Lambda表达式的。而InnerClassLambdaMetafactory的buildCallSite()方法证明了这一点,buildCallSite()方法太长,这里就不贴代码了,总之它会调用一个叫做spinInnerClass()的方法,正是这个方法使用字节码工具在内存中生成了一个类。

观察spinInnerClass()生成的类

如果我们在启动JVM的时候设置系统属性"jdk.internal.lambda.dumpProxyClasses"的话,spinnerClass()方法就会将生成的类保存到文件中,以方便调试。如果我们用下面的命令运行LambdaImplTest,就能在“当前目录”看到这个类:

java -Djdk.internal.lambda.dumpProxyClasses LambdaImplTest
LambdaImplTest$$Lambda$1.class
同样可以使用javap命令来反编译这个class文件,下面是反编译结果(我自己把javap结果转成了java文件):

final class LambdaImplTest$$Lambda$1 implements java.util.Comparator {
    
    private LambdaImplTest$$Lambda$1() {

    }

    public int compare(Object a, Object b) {
        return LambdaImplTest.lambda$m1$0:((String) a, (String) b);
    }

}
可见这个内部类实现了Comparator接口,compare()方法只是调用lambda$m1$0()方法而已。继续分析buildCallSite()方法可知,JVM接着实例化了这个内部类的一个实例,然后创建了一个ConstantCallSite,它的目标MethodHandle指向内部类实例的compare()方法。

示意图

文字描述很难说清楚问题,下面我画了一张示意图:


总结

上面分析了最简单的Lambda表达式的Java内部实现,如果Lambda表达式捕获了外部参数的话,情况会稍微复杂一点。不过结论仍然不变:Java8底层也是以内部类的方式来实现Lambda表达式的。