浅谈Lambda Expression

96
HotAsFuck
2017.09.25 00:09 字数 1907

java里的Lambda Expression想必大家都已经很清楚是个什么东西了(什么?不清楚?不清楚我也不多BB)。Lambda Expression就是被解析为functional interface的实现类,实现了它有且仅有的一个抽象方法(exactly one abstract method)。

Expression在java中提出是在JSR 335中,那么自然而然,要实现这个玩意儿就会有几种方法,比如说内部类,动态代理之类的。但是这里有两个重要目标:

1.maximizing flexibility for future optimization by not committing to a specific strategy;2.providing stability in the classfile representation.

这两个目标看起来似乎互不兼容,但是使用invokedynamic instruction来实现,就可以完美解决。Lambda Expression依赖了一些JSR 292中的特性,比如invokedynamic和method handles等等...

接着就简单看看如何用invokedynamics实现Lambda。别说了,先来几个术语压压惊:
1.dynamic call site
程序中出现lambda的地方都被称作dynamic call site,例如:

    new Thread(() -> {
            System.out.printf("Im a stateless lambda");
        }).start();

2.bootstrap method
java里对所有Lambda的有统一的bootstrap方法(LambdaMetafactory.metafactory):

    public static CallSite metafactory(MethodHandles.Lookup caller,
                                       String invokedName,
                                       MethodType invokedType,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType)

bootstrap运行期动态生成了匿名类,将其与CallSite绑定,得到了一个获取匿名类实例的call site object。
3.call site object
call site object持有MethodHandle的引用作为它的target,它是bootstrap method方法成功调用后的结果,将会与 dynamic call site永久绑定。call site object的target会被JVM执行,就如同执行一条invokevirtual指令,其所需的参数也会被压入operand stack。最后会得一个实现了functional interface的对象。

够了够了,直接来看Lambda在编译期做的事儿。首先会有个desugar的操作,就是把Lambda body转换成一个方法,方法有跟Lambda Expression对应的参数和返回值(可能会有额外的参数,比如你用到了外部变量)。生成的方法就叫做desugared method,方法大体可以分为两种类型:一种叫non-instance-capturing,意思是表达式里没有用到外部引用(this, super或者外部类的成员属性),这种会被转换为私有的静态方法;另外一种就是instance-capturing,跟上一个正好相反,会被转换成私有的实例方法。比如说这里有个表达式:

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.printf("Im a stateless lambda");
        }).start();
        Thread.sleep(100);
    }

编译之后就会在同类生成对应的non-instance-capturing方法:

 private static synthetic lambda$main$0()V
 方法体省略。。。

synthetic flag表示方法不在源码中展示。再看一个使用到了成员属性的表达式:

public class CapturedLambda{
   ……
   public String captureValue(){
        String prefix="weirdness";
       return getValue(arg -> arg+prefix+name,"1");
    }
    private <T extends String> T getValue(Function func,String num){
        return (T) func.apply(num);
    }
   ……
}

编译之后就会在同类生成对应的instance-capturing方法:

private synthetic lambda$captureValue$0(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;
方法体省略。。。

就拿这个captureValue方法里面的表达式来说,在这里会生成invokedynamic 指令:

 INVOKEDYNAMIC apply(Llambda/CapturedLambda;Ljava/lang/String;)Ljava/util/function/Function; [
      // handle kind 0x6 : 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;
      // arguments:
      (Ljava/lang/Object;)Ljava/lang/Object;, 
      // handle kind 0x7 : INVOKESPECIAL
      lambda/CapturedLambda.lambda$captureValue$0(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;, 
      (Ljava/lang/Object;)Ljava/lang/Object;
    ]

在这里invokedynamic所需要的参数是两个在run-time constant pool里的索引,它俩合起来指向的是一个CONSTANT_InvokeDynamic_info结构,在这里工具帮我展开了。然后结合之前的metafactory方法看一下参数的对应:

  1. MethodHandles.Lookup caller代表了一个能访问调用者的lookup context(在这里调用者是CapturedLambda)这个参数在调用时由VM自动压栈;
  2. String invokedName表示要实现的方法名,在这里就是Function的apply方法,调用时由VM自动压栈;
  3. MethodType invokedType告知了上面call site object它所持有的MethodHandle需要的参数和返回类型(signature),在这里是(Llambda/CapturedLambda;Ljava/lang/String;)Ljava/util/function/Function因为captureValue方法里面的表达式使用到了一个局部变量prefix和成员变量name所以有这2个参数,调用时由VM自动压栈;
  4. MethodType samMethodType表示要实现functional interface里面抽象方法的类型,在这里是Function的apply方法,即(Ljava/lang/Object;)Ljava/lang/Object;
  5. MethodHandle implMethod表示要调用的desugared method,这里就是lambda/CapturedLambda.lambda$captureValue$0(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;
  6. MethodType instantiatedMethodType即运行时的类型,因为方法定义可能是泛型,传入时可能是具体类型String之类的,要做类型校验强转等等,可以与MethodType samMethodType相同,这里是一样的,都是 (Ljava/lang/Object;)Ljava/lang/Object

接着,就进入运行期了。VM首先将上述参数压栈,调用bootstrap method,简单看一下此方法里做了什么:

 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实例,然后返回对应的CallSite对象,接着看InnerClassLambdaMetafactory:

 public InnerClassLambdaMetafactory(MethodHandles.Lookup caller,
                                       MethodType invokedType,
                                       String samMethodName,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType,
                                       boolean isSerializable,
                                       Class<?>[] markerInterfaces,
                                       MethodType[] additionalBridges)
            throws LambdaConversionException {
        super(caller, invokedType, samMethodName, samMethodType,
              implMethod, instantiatedMethodType,
              isSerializable, markerInterfaces, additionalBridges);
        implMethodClassName = implDefiningClass.getName().replace('.', '/');
        implMethodName = implInfo.getName();
        implMethodDesc = implMethodType.toMethodDescriptorString();
        implMethodReturnClass = (implKind == MethodHandleInfo.REF_newInvokeSpecial)
                ? implDefiningClass
                : implMethodType.returnType();
        constructorType = invokedType.changeReturnType(Void.TYPE);
        lambdaClassName = targetClass.getName().replace('.', '/') + "$$Lambda$" + counter.incrementAndGet();
        cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        …………

做了一些为用ASM生成匿名类所要用到的字段,类名,接口名等等的一些准备工作,然后回来看buildCallSite方法:

   CallSite buildCallSite() throws LambdaConversionException {
        final Class<?> innerClass = spinInnerClass();
        …………
    }

spinInnerClass方法是具体用ASM组装匿名类的地方,我不想贴这么多代码。。。能省略还是省略吧:

    private Class<?> spinInnerClass() throws LambdaConversionException {
        String[] interfaces;
        …………
        cw.visit(CLASSFILE_VERSION, ACC_SUPER + ACC_FINAL + ACC_SYNTHETIC,
                 lambdaClassName, null,
                 JAVA_LANG_OBJECT, interfaces);

        // Generate final fields to be filled in by constructor
        for (int i = 0; i < argDescs.length; i++) {
            FieldVisitor fv = cw.visitField(ACC_PRIVATE + ACC_FINAL,
                                            argNames[i],
                                            argDescs[i],
                                            null, null);
            fv.visitEnd();
        }
        …………

在这里生成了头部信息,即类名(根据它的生成规则,在文中的例子里是CapturedLambda$$Lambda$1),还有要实现的接口(这里是Function接口)还有访问标识乱七八糟什么的,接着生成实例字段。ASM的文档可以看ASM API。继续看~

 …………
generateConstructor();

        if (invokedType.parameterCount() != 0) {
            generateFactory();
        }

        // Forward the SAM method
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, samMethodName,
                                          samMethodType.toMethodDescriptorString(), null, null);
        mv.visitAnnotation("Ljava/lang/invoke/LambdaForm$Hidden;", true);
        new ForwardingMethodGenerator(mv).generate(samMethodType);
 …………

生成私有构造方法和如有必要生成工厂方法。在文中这个例子里需要生成工厂方法,因为用到了外部变量,这个是需要运行时传过来的,但比如是生成的是non-instance-capturing方法就不需要,因为没用到外部变量直接用构造函数。接着看~

 …………
cw.visitEnd();

        // Define the generated class in this VM.

        final byte[] classBytes = cw.toByteArray();

        // If requested, dump out to a file for debugging purposes
        if (dumper != null) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                @Override
                public Void run() {
                    dumper.dumpClass(lambdaClassName, classBytes);
                    return null;
                }
            }, null,
            new FilePermission("<<ALL FILES>>", "read, write"),
            // createDirectories may need it
            new PropertyPermission("user.dir", "read"));
        }

        return UNSAFE.defineAnonymousClass(targetClass, classBytes, null);

“生”完收工~ 最后用UNSAFE.defineAnonymousClass加载了类,这个方法每次都会返回不同的类。还有,看到了dumper这是为了调试用的,可以将生成了类保存到指定目录下,通过-Djdk.internal.lambda.dumpProxyClasses=目录

 static {
        final String key = "jdk.internal.lambda.dumpProxyClasses";
        String path = AccessController.doPrivileged(
                new GetPropertyAction(key), null,
                new PropertyPermission(key , "read"));
        dumper = (null == path) ? null : ProxyClassesDumper.getInstance(path);
    }

ok,再回到buildCallSite方法看一下余下的工作:

      …………
      if (invokedType.parameterCount() == 0) {
            final Constructor<?>[] ctrs = AccessController.doPrivileged(
                    new PrivilegedAction<Constructor<?>[]>() {
                @Override
                public Constructor<?>[] run() {
                    Constructor<?>[] ctrs = innerClass.getDeclaredConstructors();
                    if (ctrs.length == 1) {
                        // The lambda implementing inner class constructor is private, set
                        // it accessible (by us) before creating the constant sole instance
                        ctrs[0].setAccessible(true);
                    }
                    return ctrs;
                }
                    });
            if (ctrs.length != 1) {
                throw new LambdaConversionException("Expected one lambda constructor for "
                        + innerClass.getCanonicalName() + ", got " + ctrs.length);
            }

            try {
                Object inst = ctrs[0].newInstance();
                return new ConstantCallSite(MethodHandles.constant(samBase, inst));
            }
            catch (ReflectiveOperationException e) {
                throw new LambdaConversionException("Exception instantiating lambda object", e);
            }
        } else {
            try {
                UNSAFE.ensureClassInitialized(innerClass);
                return new ConstantCallSite(
                        MethodHandles.Lookup.IMPL_LOOKUP
                             .findStatic(innerClass, NAME_FACTORY, invokedType));
            }
            catch (ReflectiveOperationException e) {
                throw new LambdaConversionException("Exception finding constructor", e);
            }
        }

根据有没有构造函数参数来创建不同的CallSite,如果有直接将构造函数包装成MethodHandle作为CallSite的target,否则就运用
Lookup来查找工厂方法作为target。最后看一下生成的lambda class:

final class CapturedLambda$$Lambda$1 implements Function {
    private final CapturedLambda arg$1;
    private final String arg$2;

    private CapturedLambda$$Lambda$1(CapturedLambda var1, String var2) {
        this.arg$1 = var1;
        this.arg$2 = var2;
    }

    private static Function get$Lambda(CapturedLambda var0, String var1) {
        return new CapturedLambda$$Lambda$1(var0, var1);
    }

    @Hidden
    public Object apply(Object var1) {
        return this.arg$1.lambda$captureValue$0(this.arg$2, var1);
    }
}

实现了Function接口,构造函数接收了外部引用,有刚才说的绑定到CallSite的工厂方法,实现的apply调用的之前编译器生成的instance-capturing方法:

private synthetic lambda$captureValue$0(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;
方法体省略。。。

在invokedynamic调用完成得到CapturedLambda$$Lambda$1实例之后,就可以完成最后一步操作:

private getValue(Ljava/util/function/Function;Ljava/lang/String;)Ljava/lang/String;
   L0
    LINENUMBER 27 L0
    ALOAD 1
    ALOAD 2
    INVOKEINTERFACE java/util/function/Function.apply (Ljava/lang/Object;)Ljava/lang/Object;
    …………

通过INVOKEINTERFACE 来完成对functional interface object的调用。

结语

本文简单描述了Lambda Expression一小部分的转换和执行过程,如果有错误地方,还望无情指出。
本文其中还未涉及很多细节以及其他Lambda的特性,比如method reference capture,不只是一段表达式要被转换,一个new语句或者方法调用语句也要转换:

list.filter(String::isEmpty)

还比如AdaptationsSerialization(PS:好吧...这个我没用到,还不是太了解,手动滑稽),再比如Call Site Specifier的解析过程等等等等等......
如果想了解我上面所说的种种可以参考以下资料:

  1. http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html
  2. http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.invokedynamic
  3. http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4.10
  4. http://docs.oracle.com/javase/specs/jls/se9/html/jls-15.html#jls-15.27
  5. http://stackoverflow.com/questions/30934890/java-heap-dump-analysis-for-lambda-expressions
  6. https://blogs.oracle.com/jrose/anonymous-classes-in-the-vm

-----------------------------------------------------------------------------------------------------------------------------------------------作者:朔某人

日记本