lambda表达式原理
一、lambda表达式概述
Java 8于2014年3月发布,引入了lambda表达式作为其旗舰功能。你可能已经在你的代码库中使用它们来编写更简洁和灵活的代码。
比如在stream中使用,统计所有账单中7月的账单金额总和,如下。
int total = invoices.stream()
.filter(inv -> inv.getMonth() == Month.JULY)
.mapToInt(Invoice::getAmount)
.sum();
如果是java8之前的风格,则是使用匿名内部类,代码如下,实现的功能和lambda表达式相同。
int total = invoices.stream()
.filter(new Predicate<Invoice>() {
@Override
public boolean test(Invoice inv) {
return inv.getMonth() == Month.JULY;
}
})
.mapToInt(new ToIntFunction<Invoice>() {
@Override
public int applyAsInt(Invoice inv) {
return inv.getAmount();
}
})
.sum();
二、字节码与实现的对比
2.1 内部类
代码
import java.util.function.Function;
public class AnonymousClassExample {
Function<String, Integer> format = new Function<String, Integer>() {
public Integer apply(String input){
return Integer.parseInt(input);
}
};
}
使用javac编译后生成字节码,可以看到生成了两个class文件 AnonymousClassExample.class
和AnonymousClassExample$1.class
,后面的一个文件即为匿名内部类的class文件。匿名内部类的实现总是要创建一个匿名内部类的class文件。
javac AnonymousClassExample.java
使用javap反汇编后可以看到
javap -c AnonymousClassExample.java
public class AnonymousClassExample {
java.util.function.Function<java.lang.String, java.lang.Integer> format;
public AnonymousClassExample();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class AnonymousClassExample$1
8: dup
9: aload_0
10: invokespecial #3 // Method AnonymousClassExample$1."<init>":(LAnonymousClassExample;)V
13: putfield #4 // Field format:Ljava/util/function/Function;
16: return
}
可以看到静态内部类的实现方式总是要创建一个类实例,5: new
2.2 lambda
2.2.1 lambda的执行过程
invokedynamic 字节码指令:运行时 JVM 第一次到某个地方的这个指令的时候会进行 linkage,会调用用户指定的 Bootstrap Method 来决定要执行什么方法,之后便不需要这个步骤。 Bootstrap Method: 用户可以自己编写的方法,最终需要返回一个 CallSite 对象。 CallSite: 保存 MethodHandle 的容器,里面有一个 target MethodHandle。 MethodHandle: 真正要执行的方法的指针,即上面的生成的内部类invokespecial, invokestatic。
import java.util.function.Function;
public class Lambda {
Function<String, Integer> f = s -> Integer.parseInt(s);
}
同样javac编译javap反汇编后得到如下结果,其中不一样的就是lambda的实现使用了invokedynamic,在这里我们还看不到具体的执行方法。
public class Lambda {
java.util.function.Function<java.lang.String, java.lang.Integer> f;
public Lambda();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: invokedynamic #2, 0 // InvokeDynamic #0:apply:()Ljava/util/function/Function;
10: putfield #3 // Field f:Ljava/util/function/Function;
13: return
}
使用javap -v 后可以看到具体的调用。
每一个invokedynamic指令的实例叫做一个动态调用点(dynamic call site),动态调用点最开始是未链接状态(unlinked):表示还未指定该调用点要调用的方法), 动态调用点依靠引导方法来链接到具体的方法。引导方法是由编译器生成,在运行期当JVM第一次遇到invokedynamic指令时, 会调用引导方法来将invokedynamic指令所指定的名字(方法名,方法签名)和具体的执行代码(目标方法)链接起来, 引导方法的返回值永久的决定了调用点的行为。这一过程对应字节码中的#22(LambdaMetafactory.metafactory方法)
Constant pool:
#2 = InvokeDynamic #0:#26 // #0:apply:()Ljava/util/function/Function;
//...
#26 = NameAndType #36:#37 // apply:()Ljava/util/function/Function;
#36 = Utf8 apply
#37 = Utf8 ()Ljava/util/function/Function;
BootstrapMethods:
0: #22 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:
#23 (Ljava/lang/Object;)Ljava/lang/Object;
#24 invokestatic Lambda.lambda$new$0:(Ljava/lang/String;)Ljava/lang/Integer;
#25 (Ljava/lang/String;)Ljava/lang/Integer;
可以看到最终使用invokestatic调用了LambdaMetafactory.metafactory。
LambdaMetafactory.metafactory方法有六个参数
- MethodHandles.Lookup caller : 代表查找上下文与调用者的访问权限, 使用invokedynamic指令时,JVM会自动自动填充这个参数。
- String invokedName : 要实现的方法的名字, 使用invokedynamic时,JVM自动帮我们填充(填充内容来自常量池InvokeDynamic.NameAndType.Name)#36, 在这里JVM为我们填充为 “apply”。
- MethodType invokedType : 调用点期望的方法参数的类型和返回值的类型(方法signature)。 使用invokedynamic指令时,JVM会自动自动填充这个参数(填充内容来自常量池InvokeDynamic.NameAndType.Type) #37
- MethodType samMethodType : 函数对象将要实现的接口方法类型,这里运行时, 值为 (Object)Object 即 Consumer.accept方法的类型(泛型信息被擦除)。#23 (Ljava/lang/Object;)V
- MethodHandle implMethod : 一个直接方法句柄(DirectMethodHandle), 描述在调用时将被执行的具体实现方法 (包含适当的参数适配, 返回类型适配, 和在调用参数前附加上捕获的参数)。在这里为 #24 invokestatic Lambda.lambda$new$0:(Ljava/lang/String;)Ljava/lang/Integer; 方法的方法句柄.
- MethodType instantiatedMethodType : 函数接口方法替换泛型为具体类型后的方法类型, 通常和 samMethodType 一样, 不同的情况为泛型: #25 (Ljava/lang/String;)Ljava/lang/Integer;
public class LambdaMetafactory {
//...
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$buildCallSite,该方法最终生成的CallSite即为5:中invokedynamic的调用点,最后通过Unsafe(java的defineAnonymousClass()方法来将字节数组转换成Class对象。
CallSite buildCallSite() throws LambdaConversionException {
//生成class
final Class<?> innerClass = spinInnerClass();
//...
//构造CallSite
UNSAFE.ensureClassInitialized(innerClass);
return new ConstantCallSite(
MethodHandles.Lookup.IMPL_LOOKUP
.findStatic(innerClass, NAME_FACTORY, invokedType));
}
private Class<?> spinInnerClass() throws LambdaConversionException {
//...
UNSAFE.defineAnonymousClass(targetClass, classBytes, null);
}
看到最后发现还是构造了个内部类,那么lambda真的就是个语法糖么?其实还是有区别的,我们先来对比两种不同的lambda表达式,也就是是否有引用外部变量。
2.2.2 非捕获式non-capturing
non-capturing (the lambda doesn’t access any variables defined outside its body)
import java.util.function.Function;
public class Lambda {
Function<String, Integer> f = s -> Integer.parseInt(s);
}
字节码反汇编
java -p private
public class Lambda {
java.util.function.Function<java.lang.String, java.lang.Integer> f;
public Lambda();
private static java.lang.Integer lambda$new$0(java.lang.String);
}
java -c -v private
Constant pool:
#2 = InvokeDynamic #0:#26 // #0:apply:()Ljava/util/function/Function;
BootstrapMethods:
0: #22 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:
#23 (Ljava/lang/Object;)Ljava/lang/Object;
#24 invokestatic Lambda.lambda$new$0:(Ljava/lang/String;)Ljava/lang/Integer;
#25 (Ljava/lang/String;)Ljava/lang/Integer;
设置展示lambda之后可以看到
System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");
final class Lambda$$Lambda$1 implements Function {
private Lambda$$Lambda$1() {
}
@Hidden
public Object apply(Object var1) {
return Lambda.lambda$new$0((String)var1);
}
}
对应的静态内部类实现
class AnonymousClassExample$1 implements Function<String, Integer> {
AnonymousClassExample$1(AnonymousClassExample var1) {
this.this$0 = var1;
}
public Integer apply(String var1) {
return Integer.parseInt(var1);
}
}
可以看到这种情况下lambda简化了,没有传实例参数这一步了,而且不同的Lambda类调用都只用这个内部类即可,不像匿名内部类每次都要创建。
2.2.3 捕获式capturing
capturing (the lambda accesses variables defined outside its body).
import java.util.function.Function;
public class LambdaWithCapture {
private int offset = 10;
Function<String, Integer> f = s -> Integer.parseInt(s) + offset;
}
字节码反汇编
java -p private
public class LambdaWithCapture {
private int offset;
java.util.function.Function<java.lang.String, java.lang.Integer> f;
public LambdaWithCapture();
private java.lang.Integer lambda$new$0(java.lang.String);
}
java -c -v private
Constant pool:
#3 = InvokeDynamic #0:#30 // #0:apply:(LLambdaWithCapture;)Ljava/util/function/Function;
BootstrapMethods:
0: #26 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:
#27 (Ljava/lang/Object;)Ljava/lang/Object;
#28 invokespecial LambdaWithCapture.lambda$new$0:(Ljava/lang/String;)Ljava/lang/Integer;
#29 (Ljava/lang/String;)Ljava/lang/Integer;
设置展示lambda之后可以看到
System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");
final class LambdaWithCapture$$Lambda$2 implements Function {
private final LambdaWithCapture arg$1;
private LambdaWithCapture$$Lambda$2(Lambda var1) {
this.arg$1 = var1;
}
private static Function get$Lambda(Lambda var0) {
return new LambdaWithCapture$$Lambda$2(var0);
}
@Hidden
public Object apply(Object var1) {
return this.arg$1.lambda$new$1((String)var1);
}
}
对应的静态内部类实现
class AnonymousClassWithCapture$1 implements Function<String, Integer> {
AnonymousClassWithCapture$1(AnonymousClassWithCapture var1) {
this.this$0 = var1;
}
public Integer apply(String var1) {
return Integer.parseInt(var1) + 12;
}
}
这种情况两者的实现没有多大区别。
2.2.4 总结
两个lambda最大的区别就在于创建CallCite的MethodHandle时构造内部类的方式不同,一个不需要把调用的类实例传入且使用调用类生成的static方法,一个则需要传入且使用调用实例的成员方法。
三、总结
两者性能的比较可以分为三个部分
-
链接步骤。lambda工厂步骤与匿名内层类的类加载对比。Oracle发布了Sergey Kuksenko关于这个权衡的性能分析,你可以看到Kuksenko在2013年JVM语言峰会上发表了关于这个主题的演讲[3]。该分析表明,lambda工厂方法需要时间来预热,在这期间,它最初会比较慢。当有足够多的调用站点链接时,如果代码是在一个热的路径上(即,一个足够频繁的调用,以获得JIT编译),性能就会与类的加载一致。另一方面,如果是冷路径,lambda工厂的方法可能会快100倍。
-
捕捉周围范围的变量。如果没有变量需要捕获,那么这一步可以自动优化,以避免用基于lambda工厂的实现分配一个新对象。
-
调用实际的方法。目前,匿名内部类和lambda表达式都执行完全相同的操作,所以在这里没有性能上的差异。非捕获式lambda表达式的开箱即用的性能已经领先于悬挂式匿名内类的等价物。捕获lambda表达式的实现与为了捕获这些字段而分配一个匿名内类的性能相似。
另外lambda使用invokedynamic将lambda的实现从jvm搬到了jdk中(只需要返回一个CallCite),便于后续更改实现与拓展。