美文网首页
一文读懂lambda

一文读懂lambda

作者: 李发糕 | 来源:发表于2020-04-04 17:42 被阅读0次

    转眼间从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."<init>":()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<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;
                    }
                        });
                ```
                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<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"));
            }
    

    可以看到如果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

          本文链接:https://www.haomeiwen.com/subject/pekxnctx.html