一文读懂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."<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好处都有啥?谁说对了就给他~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容

  • 1、实例解析 先从一个例子开始: 例子很简单,定义了一个函数式接口Print ,main方法中有两处代码以Lamb...
    冰河winner阅读 1,605评论 1 1
  • Invokedynamic指令是java7中加入的字节码指令,理解这条指令可以让我们熟悉程序的执行流程,这篇文章将...
    请输入妮称阅读 2,987评论 0 1
  • inDy(invokedynamic)是 java 7 引入的一条新的虚拟机指令,这是自 1.0 以来第一次引入新...
    TiouLims阅读 18,372评论 3 31
  •  每一个class文件都对应着唯一一个类或者接口的定义信息,但是相对地,类或者接口并不一定都必须定义在文件里(比如...
    SunnyMore阅读 6,093评论 0 1
  • 代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,确实编译语言发展的一大步。 虚拟机把描述类的数据从...
    胡二囧阅读 952评论 0 0