一文读懂lambda

转眼间从java8引入的lambda也已经不再是个新鲜玩意儿了,然而笔者对它却是熟悉又陌生。网上已经有很多大佬写的相关文章,笔者今天就站在巨人们的肩膀上简单分析一下,嘿嘿嘿。
可惜水平有限,有错误的地方还望老哥们指正TT

匿名内部类

谈及lambda,就不得不提及我们在java中多次使用的匿名内部类。在lambda出现之前,作为各种回调的主要载体承载了我们的血汗。当然在今天他也同样重要,很多种情况下依然是我们的不二之选,然而在有些情况下确实可以被lambda所替代简化。那本文就先从它开刀。

public class NewTest {
    Runnable r0 = new Runnable() { //普通的匿名内部类
        @Override
        public void run() { }
    };
}

上文就是一个简单的匿名内部类,编译之后,会多出一个NewTest$1.class的文件,这个就是我们普通的匿名内部类生成的文件

使用 javap -p NewTest\$1.class 查看

Compiled from "NewTest.java"
class NewTest$1 implements java.lang.Runnable {
  final NewTest this$0;
  NewTest$1(NewTest);
  public void run();
}

显而易见的,其持有了外部类引用:this$0。也是我们开发中造成内存泄漏的一个原因。
那么,我们的lambda是否会有一些不同呢?是否只是单纯简化了匿名内部类的写法呢?

lambda

分析lambda之前,我们简单了解一下java7引入的一个概念,MethodHandle
顾名思义,代表对一个java方法的持有,可以通过invoke等方法对其持有的java方法调用,相对反射来说,更安全更快。本文只简单梳理lambda流程,想要详细了解的老哥可以去查相关资料。本文

那么,
客官里边儿请~

先将刚才的代码改成lambda写法

public class NewTest {
    Runnable r1 = ()->{ };
}

编译后通过javap -v -p NewTest.class输出class文件的详细信息
因为内容较多,这里分段分析。
首先看一下构造方法。

 public NewTest();
        ···
        0: aload_0
        1: invokespecial #1                  // Method java/lang/Object."":()V
        4: aload_0
        5: invokedynamic #2,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
        10: putfield      #3                  // Field r1:Ljava/lang/Runnable;
        13: return
       ···

首先通过invokespecial指令init实例,接下来则调用了刚刚提到的invokedynamic指令。
这个指令是干什么的呢?偷偷查了一下

每一处含有invokedynamic指令的位置都称做“动态调用点”(Dynamic Call Site),这条指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量,而是变为JDK 1.7新加入的CONSTANT_InvokeDynamic_info常量,从这个新常量中可以得到3项信息:引导方法(Bootstrap Method,此方法存放在新增的BootstrapMethods属性中)、方法类型(MethodType)和名称。引导方法是有固定的参数,并且返回值是java.lang.invoke.CallSite对象,这个代表真正要执行的目标方法调用。根据CONSTANT_InvokeDynamic_info常量中提供的信息,虚拟机可以找到并且执行引导方法,从而获得一个CallSite对象,最终调用要执行的目标方法。

简单的说,invokedynamic指令通过存放在BootstrapMethods中的引导方法(MethodHandle)获得一个CallSite对象。这个CallSite对象也持有了一个MethodHandle。通过对这个CallSite对象的MethodHandle,获得我们要的最终实例,在这里也就是Runnable实例。
我们按照顺序分析

  1. 首先根据指令的第一个参数获取对应的CONSTANT_InvokeDynamic_info常量以及其包含的信息:引导方法,方法类型,名称。
    invokedynamic #2, 0
    //先去常量池中查找对应的CONSTANT_InvokeDynamic_info常量
    #2 = InvokeDynamic #0:#23
    //这两个参数,第一个#0代表了存在BootstrapMethods中的引导方法,等下再看,第二个#23代表方法类型和名称,继续去常量池中查找
    #23 = NameAndType #29:#30
    #29 = Utf8 run
    #30 = Utf8 ()Ljava/lang/Runnable
    //正如我们刚刚代码中写的,此lambda实现的是Runnable的run方法
  2. 引导方法
    引导方法前三个参数是固定的,后面还可以附加任意数量的参数,但是参数的类型是有限制的
BootstrapMethods:
        0: #20 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;
        Method arguments:
        #21 ()V 
        #22 invokestatic NewTest.lambda$new$0:()V
        #21 ()V

这里的引导方法是java/lang/invoke/LambdaMetafactory.metafactory,自带了三个参数:
#21 ()V //我们要实现方法的(参数类型)返回类型
#22 invokestatic NewTest.lambda$new$0:()V //我们自己写的lambda实现的方法
#21 ()V //也是(参数类型)返回类型 ,但有泛形的形况下会不同,这里是会是具体的类型描述,上一个则是Ljava/lang/Object
方法返回值就是上面提到的CallSite类型。

  1. 虚拟机最终通过CallSite.makeSite方法来调用作为引导方法的MethodHandle的invoke(或invokeExact)方法,获得CallSite对象。这里我们的引导方法就是LambdaMetafactory.metafactory方法。下面我们简单分析一下这个方法。
LambdaMetafactory::metafactory
public static CallSite metafactory(MethodHandles.Lookup caller,
                                       String invokedName,//引用方法名,这里是run
                                       MethodType invokedType,//引用方法类型,这里是Runnable
                                       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();
    }

首先构建一个Lambda元工厂,在通过此原工厂生成CallSite对象返回。

InnerClassLambdaMetafactory::buildCallSite
    CallSite buildCallSite() throws LambdaConversionException {
        final Class innerClass = spinInnerClass();
        if (invokedType.parameterCount() == 0) {
            final Constructor[] ctrs = AccessController.doPrivileged(
                    new PrivilegedAction[]>() {
                @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;
                }
                    });
            ```
            try {
                Object inst = ctrs[0].newInstance();
                return new ConstantCallSite(MethodHandles.constant(samBase, inst));
            }
           ```
        } else {
            ```
        }
    }
  1. 通过spinInnerClass方法生成一个暂时我们也不知道是啥的Class对象
  2. 获取该Class的构造方法,生成该类的实例inst。
  3. 使用MethodHandles.constant方法生成对应的MethodHandle,这个MethodHandle的作用就是总是返回我们传进去的对象实例inst,使用CallSite包装并返回。
    这里的重点应该就是那个我们也不知道是啥的类了嘿嘿嘿。
InnerClassLambdaMetafactory::spinInnerClass

这个方法有点长,简单的说就是
根据生成此Lambda元工厂时设置的各种相关信息,通过ClassWriter生成对应的byte数组,最后通过UNSAFE.defineAnonymousClass注入得到对应的Class。因为是运行期间生成的,我们也看不到对应的class文件,咋办呢?
在这个方法中,有一段代码

if (dumper != null) {
            AccessController.doPrivileged(new PrivilegedAction() {
                @Override
                public Void run() {
                    dumper.dumpClass(lambdaClassName, classBytes);
                    return null;
                }
            }, null,
            new FilePermission("<>", "read, write"),
            // createDirectories may need it
            new PropertyPermission("user.dir", "read"));
        }

可以看到如果dumper!=null,就会把生成的文件输出了。那么,如何设置dumper?

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);
    }

可以看到通过设置jdk.internal.lambda.dumpProxyClasses->path则会生成dumper实例。
这里我改了一下之前的代码

public class NewTest {
    public static void main(String[] args) {
        System.getProperties().put("jdk.internal.lambda.dumpProxyClasses", "src");
        Runnable r1 = ()->{ };
    }
}

运行即可在主目录输出我们重要的class文件了,用idea反编译看看

final class NewTest$$Lambda$1 implements Runnable {
    private NewTest$$Lambda$1() {
    }

    @Hidden
    public void run() {
        NewTest.lambda$main$0();
    }
}

可以看到此类继承了我们的Runnable,实现了run方法。但是run方法的实现并不是我们在代码中写的,我们根本就是写的空实现啊。
那么,回过头来,再看看引导方法的倒数第二个参数:
#22 invokestatic NewTest.lambda$new$0:()V
正是生成的class类run方法的实现!
( 你们不要说我睁着眼睛说瞎话TT因为后面设置输出路径的时候更改了代码,把lambda的声明给放到main里面去了,所以名字长得不一样。。都写到这了我是撒泼不想改了,就是一个东西嘿嘿嘿。 拍胸脯.gif
那么这个方法在那里呢?
回到我们生成的NewTest的字节码信息中看,发现了这个方法

 private static void lambda$main$0();
    descriptor: ()V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 16: 0

果然方法里啥也没干!就是我们写的lambda实现~。
读到这里我们就大致梳理完了,总结一下:
1.查找引导方法
2.通过Callsite.makeSite方法创建对应的class类并实例化,将其用Callsite及MethodHandle包装后返回。
3.通过对callsite的调用获得刚才创建的对应的类的实例(这一步我并没有找到证据,网上看来的TT,不过通过debug获得的的确是运行时创建的类的实例)

最后

Lambda好处都有啥?谁说对了就给他~

你可能感兴趣的:(一文读懂lambda)