动态函数接口的调用原理

本篇将从编译,执行层面为大家讲解函数式接口运行的机制,让各位小伙伴更进一步加深对函数式接口的理解
概述函数式接口包含三部分内容:
(应用篇一jdk源码解析——深入函数式接口(应用篇一))(1)函数式接口的来源,(2)lambda表达式,(3)双冒号运算符(应用篇二函数式编程,这样学就废了)(4)详细介绍@functioninterface注解(5)对java.util.function包进行解读(原理篇)介绍函数式接口的实现原理 在看本篇之前,请大家对应先看应用篇一和应用篇二,本篇作为原理篇,将为大家较为深入的剖析函数式接口如何编译,jvm又是如何关联衔接各个部分的。说明:源码使用的版本为jdk-11.0.11
编译首先我们从编译出发,因为无论是接口还是类,都需要经过编译,然后在运行期由jvm执行调用,现在我们来看看几个关键位置的编译结果。先来看函数式接口编译
classfile /o:/scm/ws-java/sample-lambda/bin/com/tree/sample/func/ifuncinterfacesample.class last modified 2021-6-4; size 238 bytes md5 checksum 58a3c8c5cbe9c7498e86d4a349554ae0 compiled from ifuncinterfacesample.javapublic interface com.tree.sample.func.ifuncinterfacesample minor version: 0 major version: 55 flags: acc_public, acc_interface, acc_abstractconstant pool: #1 = class #2 // com/tree/sample/func/ifuncinterfacesample #2 = utf8 com/tree/sample/func/ifuncinterfacesample #3 = class #4 // java/lang/object #4 = utf8 java/lang/object #5 = utf8 func1 #6 = utf8 ()v #7 = utf8 sourcefile #8 = utf8 ifuncinterfacesample.java #9 = utf8 runtimevisibleannotations #10 = utf8 ljava/lang/functionalinterface;{ public abstract void func1(); descriptor: ()v flags: acc_public, acc_abstract}sourcefile: ifuncinterfacesample.javaruntimevisibleannotations: 0: #10()接口的编译信息中没有任何额外的工作,如果显示声明了functioninterface注解,则编译信息中带有,反之则无。
接下来,我们着重来看应用部分的代码编译的情况,先看应用部分的源代码:
public class lambdabinarycode { private int lambdavar = 100; public static void main(string[] args) { lambdabinarycode ins = new lambdabinarycode(); ins.invokelambda(); ins.invokeeta(); ins.invokelambda2(); } /** * 简单的函数式编程示例 */ public void invokelambda() { // 准备测试数据 integer[] data = new integer[] {1, 2, 3}; list list = arrays.aslist(data); // 简单示例:打印list数据 list.foreach(x - > system.out.println(string.format(cents into yuan: %.2f, x/100.0))); } /** * 简单的函数式编程示例 */ public void invokeeta() { // 准备测试数据 integer[] data = new integer[] {1, 2, 3}; list list = arrays.aslist(data); // 通过eta操作符访问 list.foreach(system.out::println); } /** * 简单的函数式编程示例 */ public void invokelambda2() { // 准备测试数据 map map = new hashmap(); int count = 10; random r = new random(); while(count-- >0) { map.put(r.nextint(100), r.nextint(10000)); } // lambda调用示例 map.foreach((x, y) - > { system.out.println(string.format(map key: %1s, value: %2s, x, y+lambdavar)); }); }}这段源码中选取了几种典型的场景进行组合,让大家了解更多的扩展知识,因此代码稍显长。
invokelambda() 单个参数的lambda表达式,省略参数括号和表达式主体的花括号。invokeeta() eta方式的方法引用。invokelambda2() 两个参数的lambda表达式,lambda中使用成员变量。lambda表达式的编译指北君和大家一起看看编译后的内容,使用命令查看编译后的方法结构(javap -p com.tree.sample.func.lambdabinarycode)
compiled from lambdabinarycode.javapublic class com.tree.sample.func.lambdabinarycode { private int lambdavar; public com.tree.sample.func.lambdabinarycode(); public static void main(java.lang.string[]); public void invokelambda(); public void invokeeta(); public void invokelambda2(); private static void lambda$0(java.lang.integer); private void lambda$2(java.lang.integer, java.lang.integer);}小伙伴有没发现,class文件中比源码文件中多出了两个方法:lambda2。这两个方法分别对应invokelambda和invokelambda2中的的lambda表达式。
我们在javap命令中增加-v参数,可以查看到增加的 方法的更多细节,不熟悉jvm指令的小伙伴也不用担心,我们只是验证 就是invokelambda中lambda表达式对应“x -> system.out.println(string.format(cents into yuan: %.2f, x/100.0))”。
private static void lambda$0(java.lang.integer); descriptor: (ljava/lang/integer;)v flags: acc_private, acc_static, acc_synthetic code: stack=9, locals=1, args_size=1 0: getstatic #61 // field java/lang/system.out:ljava/io/printstream; 3: ldc #105 // string cents into yuan: %.2f 5: iconst_1 6: anewarray #3 // class java/lang/object 9: dup 10: iconst_0 11: aload_0 12: invokevirtual #107 // method java/lang/integer.intvalue:()i 15: i2d 16: ldc2_w #111 // double 100.0d 19: ddiv 20: invokestatic #113 // method java/lang/double.valueof:(d)ljava/lang/double; 23: aastore 24: invokestatic #118 // method java/lang/string.format:(ljava/lang/string;[ljava/lang/object;)ljava/lang/string; 27: invokevirtual #124 // method java/io/printstream.println:(ljava/lang/string;)v 30: return linenumbertable: line 30: 0 localvariabletable: start length slot name signature 0 31 0 x ljava/lang/integer;从编译信息中我们可以看到几条明显相同的逻辑:
localvariabletable 首先包含了函数的输入参数,并且一致24行执行string.format方法27行执行printstream.println方法 从上面三个关键部分我们可以确定就是invokelambda方法中的lambda表达式编译后的内容了。仔细的小伙伴比较 和 两个方法后,可能会发现两个问题:
两个方法怎么一个是static一个是非static的呢?方法命名中的数字为什么不是数字连续的?
对于第一个问题,比较invokelambda和invokelambda2的源码,小伙伴发现有什么不同么?是否可以看到invokelambda2中的lambda表达式引用了成员属性lambdavar。这就是lambda生成方法的一种逻辑, 未使用成员变量的lambda表达式编译成静态方法,使用了成员变量的lambda语句则编译为成员方法 。第二个问题我们将留待后面回答。
lambda调用上面我们看到了lambda表达式的代码编译成了一个独立方法,指北君继续带领大家查看编译后的文件,我们要了解编译后lambda方法是如何调用执行的。查看invokelambda方法的编译后的内容(直贴出了关键部分):
public void invokelambda(); descriptor: ()v flags: acc_public code: stack=4, locals=4, args_size=1 ... ... 32: istore_3 33: aload_2 34: invokedynamic #45, 0 // invokedynamic #0:accept:()ljava/util/function/consumer; 39: invokeinterface #49, 2 // interfacemethod java/util/list.foreach:(ljava/util/function/consumer;) ... ...在invokelambda中有一个指令invokedynamic,熟悉动态语言的小伙伴可能知道,这个指令是java7为支持动态脚本语言而增加的。而函数式java调用函数接口也正是通过invokedynamic指令来实现的。invokelambda的详细内容指北君后续单独为大家讲解,今天我们关注函数接口的调用过程。
使用invokelambda指令,那么该指令是直接调用的lambda$0方法么?我们知道list.foreach(xx)调用中,我们是将函数接口作为参数传递到其他类的函数中进行执行的。java需要解决两个问题:
1)如何将方法传递给被调用的外部类的方法。
2)外部的类和方法如何访问我们内部私有的方法。
引导方法表为解决上面两个问题,我们继续查编译后的文件,在末尾,我们看到下面的部分:
bootstrapmethods: 0: #146 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: #148 (ljava/lang/object;)v #151 invokestatic com/tree/sample/func/lambdabinarycode.lambda$0:(ljava/lang/integer;)v #152 (ljava/lang/integer;)v 1: #146 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: #153 (ljava/lang/object;)v #156 invokevirtual java/io/printstream.println:(ljava/lang/object;)v #157 (ljava/lang/integer;)v 2: #146 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: #159 (ljava/lang/object;ljava/lang/object;)v #162 invokespecial com/tree/sample/func/lambdabinarycode.lambda$2:(ljava/lang/integer;ljava/lang/integer;)v #163 (ljava/lang/integer;ljava/lang/integer;)vinnerclasses: public static final #169= #165 of #167; //lookup=class java/lang/invoke/methodhandles$lookup of class java/lang/invoke/methodhandles这生成了三个引导方法,刚好和我们的三个函数接口调用一致,从引导方法的参数我们看出
序号调用调用类型
0 lambda$0 static
1 printstream.println vertual
2 lambda$2 special
顺便回答一下之前的方法名称的数字序号不连续问题,我们看出,方法名称的序号是根据引导方法的序号来确定的,不是根据生成的lambda表达式方法序号来的。我们看到,引导方法的逻辑似乎就是调用lambda方法或者其他的函数接口,每个引导方法中都出现了lambdametafactory.metafactory方法
动态调用现在,我们结合invokedynamic指令来说明bootstrapmethods执行的过程
动态调用逻辑
上面的的流程显示了动态调用的基本逻辑
执行invokedynamic检查调用点是否已连接可用如果未连接,构建动态调用点执行引导方法生成并加载调用点对应的动态内部类连接调用动态内部类方法内部类调用lambda对应的方法并执行这两个阶段我们通过调用堆栈也能明显观察到:
引导阶段
执行阶段
我们还可以通过设置vm参数-djdk.internal.lambda.dumpproxyclasses,查看以引导阶段动态生成的内部类:
动态内部类列表
打开其中一个如下:
动态内部类详情
小结动态函数接口的调用原理,给大家介绍到这里了,相信大家看完本篇内容后,对函数式接口有了更深一层的学习。由于涉及的内容较多,没有时间给大家逐一详细的给每个涉及到的类进行解读。后续指北君会根据小伙伴们需要对今天提及的知识点做深入的阶段,比如invokeddynamic指令,class结构,动态调用相关的各部分代码逻辑。

vivo APEX 2020有哪些值得期待的地方
VOC和PM2.5传感器对睡眠的帮助
2018亚太区块链峰会(夏季场)东京热辣来袭!
特斯拉:好的作品是看得见的
从华为海思麒麟看中国半导体崛起之路
动态函数接口的调用原理
英特尔对眼神追踪技术公司Tobii投资2100万美元
2020Medtec中国展如期举行 近50+知名企业已组团报名现场参观
“2020中国新材料资本技术大会”回顾
慕容话币|新手如何进入区块链行业?
一文分享OCS实现方法
再创新高!华为数据中心交换机市场份额43.85%,蝉联第一!
人工智能如何变得更加的务实
人工智能的10大事件详细概述
小米9透明尊享版高清图集
CAN通信设备如何进行批量高效老化测试
如何估算出电线能承受的电流四种方法详解
云米的智能家电将实现全屋互联
详解石墨烯生物医用领域的应用
中国4G将大举来袭,各厂商严阵以待