java8新特性Lambda表达式为什么运⾏效率低
Lambda表达式为什么运⾏效率低
准备
我为什么说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++) {
<(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为什么使用bootstrap?
字节码指令执⾏步骤:
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/afactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava 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/afactory。
代码如下:
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⽅法是会⽣成⼀个内部类并且⽣成该类的⼀个实例。
那么现在要⽣成⼀个内部类,需要⼀些什么条件呢:
1. 类名:可按⼀些规则⽣成
2. 类需要实现的接⼝:编译时就已知了,本例中就是Consumer接⼝
3. 实现接⼝⾥⾯的⽅法:本例中就是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表达式,只是因为我们例⼦中L
ambda表达式没写什么逻辑,所以这段字节码指令Code部分也没有什么内容。那么,我们现在在实现内部类中的void accept(T t)⽅法时,只要调⽤⼀个这个lambda$main$0静态⽅法即可。
所以到此,⼀个内部类就可以被正常的实现出来了,内部类有了之后,Lambda表达式就是可以被转换成这个内部类的对象,就可以进⾏循环了。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论