网站首页 > 技术文章 正文
准备
我为什么说Lambda表达式运行效率低。
先准备一个list:
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
list.add(i);
}
先用Lambda表达式的方式来循环一下这个list:
long lambdaStart = System.currentTimeMillis();
list.forEach(i -> {
// 不用做事情,循环就够了
});
long lambdaEnd = System.currentTimeMillis();
System.out.println("lambda循环运行毫秒数===" + (lambdaEnd - lambdaStart));
运行时间大概为110ms
再用普通方式来循环一下这个list:
long normalStart = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {
// 不用做事情,循环就够了
}
long normalEnd = System.currentTimeMillis();
System.out.println("普通循环运行毫秒数===" + (normalEnd - normalStart));
运行时间大概为0ms或1ms
你们没看错,运行时间差别就是这么大,不相信的话大家可以自己去试一下,并且这并不是只有在循环时使用Lambda表达式才会导致运行效率低,而是Lambda表达式在运行时就是会需要额外的时间,我们继续来分析。
分析
如果我们要研究Lambda表达式,最正确、最直接的方法就是查看它所对应的字节码指令。
使用以下命令查看class文件对应的字节码指令:
javap -v -p Test.class
上述命令解析出来的指令非常多,我这里提取比较重要的部分来给大家分析:
使用Lambda表达式所对应的字节码指令如下:
34: invokestatic #6 // Method java/lang/System.currentTimeMillis:()J
37: lstore_2
38: aload_1
39: invokedynamic #7, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
44: invokeinterface #8, 2 // InterfaceMethod java/util/List.forEach:(Ljava/util/function/Consumer;)V
49: invokestatic #6 // Method java/lang/System.currentTimeMillis:()J
不使用Lambda表达式所对应的字节码指令如下:
82: invokestatic #6 // Method java/lang/System.currentTimeMillis:()J
85: lstore 6
87: iconst_0
88: istore 8
90: iload 8
92: aload_1
93: invokeinterface #17, 1 // InterfaceMethod java/util/List.size:()I
98: if_icmpge 107
101: iinc 8, 1
104: goto 90
107: invokestatic #6 // Method java/lang/System.currentTimeMillis:()J
从上面两种方式所对应的字节码指令可以看出,两种方式的执行方式确实不太一样。
不使用Lambda表达式执行循环流程
字节码指令执行步骤:
- 82:invokestatic: 执行静态方法,java/lang/System.currentTimeMillis:();
- 85-92: 简单来说就是初始化数据,int i = 0;
- 93:invokeinterface:执行接口方法,接口为List,所以真正执行的是就是ArrayList.size方法;
- 98:if_icmpge: 比较,相当于执行i < list.size();
- 101:iinc: i++;
- 104:goto: 进行下一次循环;
- 107:invokestatic: 执行静态方法;
那么这个流程大家应该问题不大,是一个很正常的循环逻辑。
使用Lambda表达式执行循环流程
我们再来看一下对应的字节码指令:
34: invokestatic #6 // Method java/lang/System.currentTimeMillis:()J
37: lstore_2
38: aload_1
39: invokedynamic #7, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
44: invokeinterface #8, 2 // InterfaceMethod java/util/List.forEach:(Ljava/util/function/Consumer;)V
49: invokestatic #6 // Method java/lang/System.currentTimeMillis:()J
字节码指令执行步骤:
- 34: invokestatic: 执行静态方法,java/lang/System.currentTimeMillis:();
- 37-38: 初始化数据
- 39: invokedynamic: 这是在干什么?
- 44: invokeinterface: 执行java/util/List.forEach()方法
- 49: invokestatic: 执行静态方法,java/lang/System.currentTimeMillis:();
和上面正常循环的方式的字节码指令不太一样,我们认真的看一下这个字节码指令,这个流程并不像是一个循环的流程,而是一个方法顺序执行的流程:
- 先初始化一些数据
- 执行invokedynamic指令(暂时这个指令是做什么的)
- 然后执行java/util/List.forEach()方法,所以真正的循环逻辑在这里
所以我们可以发现,使用Lambda表达式循环时,在循环前会做一些其他事情,所以导致执行时间要更长一点。
那么invokedynamic指令到底做了什么事情呢?
java/util/List.forEach方法接收一个参数Consumer<? super T> action,Consumer是一个接口,所以如果要调用这个方法,就要传递该接口类型的对象。
而我们在代码里实际上是传递的一个Lambda表达式,那么我们这里可以假设:需要将Lambda表达式转换成对象,且该对象的类型需要根据该Lambda表达式所使用的地方在编译时期进行反推。
这里在解释一下反推:一个Lambda表达式是可以被多个方法使用的,而且这个方法所接收的参数类型,也就是函数式接口,是可以不一样的,只要函数式接口符合该Lambda表达式的定义即可。
本例中,编译器在编译时可以反推出,Lambda表达式对应一个Cosumer接口类型的对象。
那么如果要将Lambda表达式转换成一个对象,就需要有一个类实现Consumer接口。
所以,现在的问题就是这个类是什么时候生成的,并且生成在哪里了?
所以,我们慢慢的应该能够想到,invokedynamic指令,它是不是就是先将Lambda表达式转换成某个类,然后生成一个实例以便提供给forEach方法调用呢?
我们回头再看一下invokedynamic指令:
invokedynamic #7, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
Java中调用函数有四大指令:invokevirtual、invokespecial、invokestatic、invokeinterface,在JSR 292 添加了一个新的指令invokedynamic,这个指令表示执行动态语言,也就是Lambda表达式。
该指令注释中的#0表示的是BootstrapMethods中的第0个方法:
BootstrapMethods:
0: #60 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:
#61 (Ljava/lang/Object;)V
#62 invokestatic com/luban/Test.lambda$main$0:(Ljava/lang/Integer;)V
#63 (Ljava/lang/Integer;)V
所以invokedynamic执行时,实际上就是执行BootstrapMethods中的方法,比如本例中的:java/lang/invoke/LambdaMetafactory.metafactory。
代码如下:
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();
}
这个方法中用到了一个特别明显且易懂的类:InnerClassLambdaMetafactory。
这个类是一个针对Lambda表达式生成内部类的工厂类。当调用buildCallSite方法是会生成一个内部类并且生成该类的一个实例。
那么现在要生成一个内部类,需要一些什么条件呢:
- 类名:可按一些规则生成
- 类需要实现的接口:编译时就已知了,本例中就是Consumer接口
- 实现接口里面的方法:本例中就是Consumer接口的void accept(T t)方法。
那么内部类该怎么实现void accept(T t)方法呢?
我们再来看一下javap -v -p Test.class的结果中除开我们自己实现的方法外还多了一个方法:
private static void lambda$main$0(java.lang.Integer);
descriptor: (Ljava/lang/Integer;)V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 25: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 i Ljava/lang/Integer;
很明显,这个静态的lambda$main$0方法代表的就是我们写的Lambda表达式,只是因为我们例子中Lambda表达式没写什么逻辑,所以这段字节码指令Code部分也没有什么内容。
那么,我们现在在实现内部类中的void accept(T t)方法时,只要调用一个这个lambda$main$0静态方法即可。
所以到此,一个内部类就可以被正常的实现出来了,内部类有了之后,Lambda表达式就是可以被转换成这个内部类的对象,就可以进行循环了。
创作不易,觉得写的不错的,还请多多转发
猜你喜欢
- 2024-09-21 Java并发编程:LongAdder | LongAccumulator 对比测试
- 2024-09-21 「Java技巧」优雅的统计程序的执行时间,别再用System.cur
- 2024-09-21 Flink SQL 知其所以然(九)| SQL 的时间语义
- 2024-09-21 ArrayList插入1000w条数据之后,我怀疑了jvm...
- 2024-09-21 《Java实战之内存模型》详解篇(java内存模型happens before)
- 2024-09-21 比反射更快!使用ASM获取class信息(ClassReader)
- 2024-09-21 了解Java线程优先级,更要知道对应操作系统的优先级,不然会踩坑
- 2024-09-21 让大学生写的一个计算时间的方法,有人看得出来是在做什么吗?这
- 2024-09-21 Java基础——Java多线程(Lock接口详解)
- 2024-09-21 JVM性能调优监控工具jps、jstack、jmap、jhat、jstat使用详解
- 1514℃桌面软件开发新体验!用 Blazor Hybrid 打造简洁高效的视频处理工具
- 569℃Dify工具使用全场景:dify-sandbox沙盒的原理(源码篇·第2期)
- 510℃MySQL service启动脚本浅析(r12笔记第59天)
- 486℃服务器异常重启,导致mysql启动失败,问题解决过程记录
- 485℃启用MySQL查询缓存(mysql8.0查询缓存)
- 467℃「赵强老师」MySQL的闪回(赵强iso是哪个大学毕业的)
- 446℃mysql服务怎么启动和关闭?(mysql服务怎么启动和关闭)
- 444℃MySQL server PID file could not be found!失败
- 最近发表
- 标签列表
-
- c++中::是什么意思 (83)
- 标签用于 (65)
- 主键只能有一个吗 (66)
- c#console.writeline不显示 (75)
- pythoncase语句 (81)
- es6includes (73)
- windowsscripthost (67)
- apt-getinstall-y (86)
- node_modules怎么生成 (76)
- c++int转char (75)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- js判断是否是json字符串 (67)
- checkout-b (67)
- c语言min函数头文件 (68)
- asynccallback (71)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- & (66)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- eacces (67)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)