运行时级别的 lambda 和方法引用有什么区别?
What is the difference between a lambda and a method reference at a runtime level?
我遇到了使用方法引用而不是 lambda 发生的问题。该代码如下:
(Comparator<ObjectNode> & Serializable) SOME_COMPARATOR::compare
或者,使用 lambda,
(Comparator<ObjectNode> & Serializable) (a, b) -> SOME_COMPARATOR.compare(a, b)
从语义上讲,它是完全相同的,但实际上它是不同的,因为在第一种情况下,我在 Java 序列化 类 中得到一个异常。我的问题不是关于这个异常,因为实际代码是 运行 在一个更复杂的上下文中,它被证明在序列化方面有奇怪的行为,所以如果我提供更多细节,它只会让人很难回答。
我想了解的是这两种创建 lambda 表达式的方式之间的区别。
入门
为了对此进行调查,我们从以下内容开始 class:
import java.io.Serializable;
import java.util.Comparator;
public final class Generic {
// Bad implementation, only used as an example.
public static final Comparator<Integer> COMPARATOR = (a, b) -> (a > b) ? 1 : -1;
public static Comparator<Integer> reference() {
return (Comparator<Integer> & Serializable) COMPARATOR::compare;
}
public static Comparator<Integer> explicit() {
return (Comparator<Integer> & Serializable) (a, b) -> COMPARATOR.compare(a, b);
}
}
编译后,我们可以反汇编使用:
javap -c -p -s -v Generic.class
删除不相关的部分(以及其他一些混乱,例如完全限定类型和 COMPARATOR
的初始化)我们剩下
public static final Comparator<Integer> COMPARATOR;
public static Comparator<Integer> reference();
0: getstatic #2 // Field COMPARATOR:LComparator;
3: dup
4: invokevirtual #3 // Method Object.getClass:()LClass;
7: pop
8: invokedynamic #4, 0 // InvokeDynamic #0:compare:(LComparator;)LComparator;
13: checkcast #5 // class Serializable
16: checkcast #6 // class Comparator
19: areturn
public static Comparator<Integer> explicit();
0: invokedynamic #7, 0 // InvokeDynamic #1:compare:()LComparator;
5: checkcast #5 // class Serializable
8: checkcast #6 // class Comparator
11: areturn
private static int lambda$explicit$d34e1a25(Integer, Integer);
0: getstatic #2 // Field COMPARATOR:LComparator;
3: aload_0
4: aload_1
5: invokeinterface #44, 3 // InterfaceMethod Comparator.compare:(LObject;LObject;)I
10: ireturn
BootstrapMethods:
0: #61 invokestatic invoke/LambdaMetafactory.altMetafactory:(Linvoke/MethodHandles$Lookup;LString;Linvoke/MethodType;[LObject;)Linvoke/CallSite;
Method arguments:
#62 (LObject;LObject;)I
#63 invokeinterface Comparator.compare:(LObject;LObject;)I
#64 (LInteger;LInteger;)I
#65 5
#66 0
1: #61 invokestatic invoke/LambdaMetafactory.altMetafactory:(Linvoke/MethodHandles$Lookup;LString;Linvoke/MethodType;[LObject;)Linvoke/CallSite;
Method arguments:
#62 (LObject;LObject;)I
#70 invokestatic Generic.lambda$explicit$df5d232f:(LInteger;LInteger;)I
#64 (LInteger;LInteger;)I
#65 5
#66 0
我们立即看到 reference()
方法的字节码与 explicit()
的字节码不同。然而,显着差异 isn't actually relevant,但 bootstrap 方法很有趣。
An invokedynamic call site is linked to a method by means of a bootstrap method, which is a method specified by the compiler for the dynamically-typed language that is called once by the JVM to link the site.
(Java Virtual Machine Support for Non-Java Languages, 强调他们的)
这是负责创建 CallSite used by the lambda. The Method arguments
listed below each bootstrap method are the values passed as the variadic parameter (i.e. args
) of LambdaMetaFactory#altMetaFactory 的代码。
方法参数的格式
- samMethodType - 函数对象要实现的方法的签名和 return 类型。
- implMethod - 一个直接方法句柄,描述在调用时应调用的实现方法(适当调整参数类型,return 类型,并将捕获的参数添加到调用参数之前)。
- instantiatedMethodType - 应在调用时动态实施的签名和 return 类型。这可能与 samMethodType 相同,或者可能是它的特化。
- flags 表示附加选项;这是所需标志的按位或。定义的标志是 FLAG_BRIDGES、FLAG_MARKERS 和 FLAG_SERIALIZABLE.
- bridgeCount 是函数对象应实现的附加方法签名的数量,当且仅当设置了 FLAG_BRIDGES 标志时才存在。
在这两种情况下,bridgeCount
都是 0,所以没有 6,否则会是 bridges
- 要实现的附加方法签名的可变长度列表(假定 bridgeCount
是 0,我不完全确定为什么设置 FLAG_BRIDGES。
将以上内容与我们的论点相匹配,我们得到:
- 函数签名和 return 类型
(Ljava/lang/Object;Ljava/lang/Object;)I
,这是 Comparator#compare 的 return 类型,因为泛型类型擦除。
- 调用此 lambda 时调用的方法(不同)。
- lambda 的签名和return 类型,将在调用 lambda 时检查:
(LInteger;LInteger;)I
(请注意,这些不会被删除,因为这是 lambda 规范的一部分).
- 标志,在这两种情况下都是FLAG_BRIDGES and FLAG_SERIALIZABLE(即5)的组成。
- 桥接方法签名数量,0。
我们可以看到为两个 lambda 都设置了 FLAG_SERIALIZABLE,所以不是那样。
实现方法
方法引用 lambda 的实现方法是 Comparator.compare:(LObject;LObject;)I
,但是显式 lambda 的实现方法是 Generic.lambda$explicit$df5d232f:(LInteger;LInteger;)I
。看反汇编可以看出,前者本质上是后者的内联版本。唯一的其他显着区别是方法参数类型(如前所述,这是因为泛型类型擦除)。
lambda 何时真正可序列化?
You can serialize a lambda expression if its target type and its captured arguments are serializable.
Lambda Expressions (The Java™ Tutorials)
其中重要的部分是 "captured arguments"。回顾反汇编的字节码,方法引用的 invokedynamic 指令看起来确实像是在捕获比较器(#0:compare:(LComparator;)LComparator;
,与显式 lambda 形成对比,#1:compare:()LComparator;
)。
确认捕获是问题所在
ObjectOutputStream
包含一个 extendedDebugInfo
字段,我们可以使用 -Dsun.io.serialization.extendedDebugInfo=true
VM 参数设置它:
$ java -Dsun.io.serialization.extendedDebugInfo=true Generic
当我们再次尝试序列化 lambda 时,结果非常令人满意
Exception in thread "main" java.io.NotSerializableException: Generic$$Lambda/321001045
- element of array (index: 0)
- array (class "[LObject;", size: 1)
/* ! */ - field (class "invoke.SerializedLambda", name: "capturedArgs", type: "class [LObject;") // <--- !!
- root object (class "invoke.SerializedLambda", SerializedLambda[capturingClass=class Generic, functionalInterfaceMethod=Comparator.compare:(LObject;LObject;)I, implementation=invokeInterface Comparator.compare:(LObject;LObject;)I, instantiatedMethodType=(LInteger;LInteger;)I, numCaptured=1])
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1182)
/* removed */
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at Generic.main(Generic.java:27)
实际发生了什么
从上面我们可以看出,显式 lambda 不 捕获任何东西,而方法引用 lambda 是。再次查看字节码可以清楚地看到:
public static Comparator<Integer> explicit();
0: invokedynamic #7, 0 // InvokeDynamic #1:compare:()LComparator;
5: checkcast #5 // class java/io/Serializable
8: checkcast #6 // class Comparator
11: areturn
如上所示,其实现方法为:
private static int lambda$explicit$d34e1a25(java.lang.Integer, java.lang.Integer);
0: getstatic #2 // Field COMPARATOR:Ljava/util/Comparator;
3: aload_0
4: aload_1
5: invokeinterface #44, 3 // InterfaceMethod java/util/Comparator.compare:(Ljava/lang/Object;Ljava/lang/Object;)I
10: ireturn
显式 lambda 实际上是调用 lambda$explicit$d34e1a25
,后者又调用 COMPARATOR#compare
。这个间接层意味着它不会捕获任何不是 Serializable
的东西(或者准确地说,根本不捕获任何东西),因此可以安全地序列化。方法引用表达式直接使用COMPARATOR
(其值然后传递给bootstrap方法):
public static Comparator<Integer> reference();
0: getstatic #2 // Field COMPARATOR:LComparator;
3: dup
4: invokevirtual #3 // Method Object.getClass:()LClass;
7: pop
8: invokedynamic #4, 0 // InvokeDynamic #0:compare:(LComparator;)LComparator;
13: checkcast #5 // class java/io/Serializable
16: checkcast #6 // class Comparator
19: areturn
缺少间接意味着 COMPARATOR
必须与 lambda 一起序列化。由于 COMPARATOR
不引用 Serializable
值,因此失败。
修复
我不愿将其称为编译器错误(我希望缺少间接寻址可以作为一种优化),尽管这很奇怪。修复是微不足道的,但很难看;在声明中为 COMPARATOR
添加显式转换:
public static final Comparator<Integer> COMPARATOR = (Serializable & Comparator<Integer>) (a, b) -> a > b ? 1 : -1;
这使得在 Java 1.8.0_45 上一切正常。还值得注意的是,eclipse 编译器也在方法引用案例中生成该间接层,因此此 post 中的原始代码不需要修改即可正确执行。
我想补充一个事实,即 lambda 和方法引用之间实际上存在语义差异 实例 方法(即使它们与您的情况具有相同的内容,并且忽略序列化):
SOME_COMPARATOR::compare
此形式计算为一个 lambda 对象,该对象在计算时间 SOME_COMPARATOR
的 值 上封闭(即,它包含对该对象的引用)。它将在评估时间检查SOME_COMPARATOR
是否为空并抛出一个空指针异常。它不会获取在创建后对字段所做的更改。
(a,b) -> SOME_COMPARATOR.compare(a,b)
此表单计算为 lambda 对象,该对象将在调用时访问 SOME_COMPARATOR
字段的值 。它在 this
上关闭,因为 SOME_COMPARATOR
是一个实例字段。调用时它将访问 SOME_COMPARATOR
的当前值并使用它,此时可能会抛出空指针异常。
示范[=46=]
从下面的小例子中可以看出这种行为。通过在调试器中停止代码并检查 lambda 的字段,可以验证它们关闭了什么。
Object o = "First";
void run() {
Supplier<String> ref = o::toString;
Supplier<String> lambda = () -> o.toString();
o = "Second";
System.out.println("Ref: " + ref.get()); // Prints "First"
System.out.println("Lambda: " + lambda.get()); // Prints "Second"
}
Java 语言规范
JLS 在 15.13.3 中描述了方法引用的这种行为:
The target reference is the value of ExpressionName or Primary, as determined when the method reference expression was evaluated.
并且:
First, if the method reference expression begins with an ExpressionName or a Primary, this subexpression is evaluated. If the subexpression evaluates to null
, a NullPointerException
is raised
在 Tobys 代码中
这可以在 reference
代码的 Tobys 列表中看到,其中 getClass
是在 SOME_COMPARATOR
的值上调用的,如果它为 null 将触发异常:
4: invokevirtual #3 // Method Object.getClass:()LClass;
(或者我认为,我真的不是字节码专家。)
然而,符合 Eclipse 4.4.1 的代码中的方法引用在那种情况下不会抛出异常。 Eclipse 似乎在这里有一个错误。
我遇到了使用方法引用而不是 lambda 发生的问题。该代码如下:
(Comparator<ObjectNode> & Serializable) SOME_COMPARATOR::compare
或者,使用 lambda,
(Comparator<ObjectNode> & Serializable) (a, b) -> SOME_COMPARATOR.compare(a, b)
从语义上讲,它是完全相同的,但实际上它是不同的,因为在第一种情况下,我在 Java 序列化 类 中得到一个异常。我的问题不是关于这个异常,因为实际代码是 运行 在一个更复杂的上下文中,它被证明在序列化方面有奇怪的行为,所以如果我提供更多细节,它只会让人很难回答。
我想了解的是这两种创建 lambda 表达式的方式之间的区别。
入门
为了对此进行调查,我们从以下内容开始 class:
import java.io.Serializable;
import java.util.Comparator;
public final class Generic {
// Bad implementation, only used as an example.
public static final Comparator<Integer> COMPARATOR = (a, b) -> (a > b) ? 1 : -1;
public static Comparator<Integer> reference() {
return (Comparator<Integer> & Serializable) COMPARATOR::compare;
}
public static Comparator<Integer> explicit() {
return (Comparator<Integer> & Serializable) (a, b) -> COMPARATOR.compare(a, b);
}
}
编译后,我们可以反汇编使用:
javap -c -p -s -v Generic.class
删除不相关的部分(以及其他一些混乱,例如完全限定类型和 COMPARATOR
的初始化)我们剩下
public static final Comparator<Integer> COMPARATOR;
public static Comparator<Integer> reference();
0: getstatic #2 // Field COMPARATOR:LComparator;
3: dup
4: invokevirtual #3 // Method Object.getClass:()LClass;
7: pop
8: invokedynamic #4, 0 // InvokeDynamic #0:compare:(LComparator;)LComparator;
13: checkcast #5 // class Serializable
16: checkcast #6 // class Comparator
19: areturn
public static Comparator<Integer> explicit();
0: invokedynamic #7, 0 // InvokeDynamic #1:compare:()LComparator;
5: checkcast #5 // class Serializable
8: checkcast #6 // class Comparator
11: areturn
private static int lambda$explicit$d34e1a25(Integer, Integer);
0: getstatic #2 // Field COMPARATOR:LComparator;
3: aload_0
4: aload_1
5: invokeinterface #44, 3 // InterfaceMethod Comparator.compare:(LObject;LObject;)I
10: ireturn
BootstrapMethods:
0: #61 invokestatic invoke/LambdaMetafactory.altMetafactory:(Linvoke/MethodHandles$Lookup;LString;Linvoke/MethodType;[LObject;)Linvoke/CallSite;
Method arguments:
#62 (LObject;LObject;)I
#63 invokeinterface Comparator.compare:(LObject;LObject;)I
#64 (LInteger;LInteger;)I
#65 5
#66 0
1: #61 invokestatic invoke/LambdaMetafactory.altMetafactory:(Linvoke/MethodHandles$Lookup;LString;Linvoke/MethodType;[LObject;)Linvoke/CallSite;
Method arguments:
#62 (LObject;LObject;)I
#70 invokestatic Generic.lambda$explicit$df5d232f:(LInteger;LInteger;)I
#64 (LInteger;LInteger;)I
#65 5
#66 0
我们立即看到 reference()
方法的字节码与 explicit()
的字节码不同。然而,显着差异 isn't actually relevant,但 bootstrap 方法很有趣。
An invokedynamic call site is linked to a method by means of a bootstrap method, which is a method specified by the compiler for the dynamically-typed language that is called once by the JVM to link the site.
(Java Virtual Machine Support for Non-Java Languages, 强调他们的)
这是负责创建 CallSite used by the lambda. The Method arguments
listed below each bootstrap method are the values passed as the variadic parameter (i.e. args
) of LambdaMetaFactory#altMetaFactory 的代码。
方法参数的格式
- samMethodType - 函数对象要实现的方法的签名和 return 类型。
- implMethod - 一个直接方法句柄,描述在调用时应调用的实现方法(适当调整参数类型,return 类型,并将捕获的参数添加到调用参数之前)。
- instantiatedMethodType - 应在调用时动态实施的签名和 return 类型。这可能与 samMethodType 相同,或者可能是它的特化。
- flags 表示附加选项;这是所需标志的按位或。定义的标志是 FLAG_BRIDGES、FLAG_MARKERS 和 FLAG_SERIALIZABLE.
- bridgeCount 是函数对象应实现的附加方法签名的数量,当且仅当设置了 FLAG_BRIDGES 标志时才存在。
在这两种情况下,bridgeCount
都是 0,所以没有 6,否则会是 bridges
- 要实现的附加方法签名的可变长度列表(假定 bridgeCount
是 0,我不完全确定为什么设置 FLAG_BRIDGES。
将以上内容与我们的论点相匹配,我们得到:
- 函数签名和 return 类型
(Ljava/lang/Object;Ljava/lang/Object;)I
,这是 Comparator#compare 的 return 类型,因为泛型类型擦除。 - 调用此 lambda 时调用的方法(不同)。
- lambda 的签名和return 类型,将在调用 lambda 时检查:
(LInteger;LInteger;)I
(请注意,这些不会被删除,因为这是 lambda 规范的一部分). - 标志,在这两种情况下都是FLAG_BRIDGES and FLAG_SERIALIZABLE(即5)的组成。
- 桥接方法签名数量,0。
我们可以看到为两个 lambda 都设置了 FLAG_SERIALIZABLE,所以不是那样。
实现方法
方法引用 lambda 的实现方法是 Comparator.compare:(LObject;LObject;)I
,但是显式 lambda 的实现方法是 Generic.lambda$explicit$df5d232f:(LInteger;LInteger;)I
。看反汇编可以看出,前者本质上是后者的内联版本。唯一的其他显着区别是方法参数类型(如前所述,这是因为泛型类型擦除)。
lambda 何时真正可序列化?
You can serialize a lambda expression if its target type and its captured arguments are serializable.
Lambda Expressions (The Java™ Tutorials)
其中重要的部分是 "captured arguments"。回顾反汇编的字节码,方法引用的 invokedynamic 指令看起来确实像是在捕获比较器(#0:compare:(LComparator;)LComparator;
,与显式 lambda 形成对比,#1:compare:()LComparator;
)。
确认捕获是问题所在
ObjectOutputStream
包含一个 extendedDebugInfo
字段,我们可以使用 -Dsun.io.serialization.extendedDebugInfo=true
VM 参数设置它:
$ java -Dsun.io.serialization.extendedDebugInfo=true Generic
当我们再次尝试序列化 lambda 时,结果非常令人满意
Exception in thread "main" java.io.NotSerializableException: Generic$$Lambda/321001045
- element of array (index: 0)
- array (class "[LObject;", size: 1)
/* ! */ - field (class "invoke.SerializedLambda", name: "capturedArgs", type: "class [LObject;") // <--- !!
- root object (class "invoke.SerializedLambda", SerializedLambda[capturingClass=class Generic, functionalInterfaceMethod=Comparator.compare:(LObject;LObject;)I, implementation=invokeInterface Comparator.compare:(LObject;LObject;)I, instantiatedMethodType=(LInteger;LInteger;)I, numCaptured=1])
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1182)
/* removed */
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at Generic.main(Generic.java:27)
实际发生了什么
从上面我们可以看出,显式 lambda 不 捕获任何东西,而方法引用 lambda 是。再次查看字节码可以清楚地看到:
public static Comparator<Integer> explicit();
0: invokedynamic #7, 0 // InvokeDynamic #1:compare:()LComparator;
5: checkcast #5 // class java/io/Serializable
8: checkcast #6 // class Comparator
11: areturn
如上所示,其实现方法为:
private static int lambda$explicit$d34e1a25(java.lang.Integer, java.lang.Integer);
0: getstatic #2 // Field COMPARATOR:Ljava/util/Comparator;
3: aload_0
4: aload_1
5: invokeinterface #44, 3 // InterfaceMethod java/util/Comparator.compare:(Ljava/lang/Object;Ljava/lang/Object;)I
10: ireturn
显式 lambda 实际上是调用 lambda$explicit$d34e1a25
,后者又调用 COMPARATOR#compare
。这个间接层意味着它不会捕获任何不是 Serializable
的东西(或者准确地说,根本不捕获任何东西),因此可以安全地序列化。方法引用表达式直接使用COMPARATOR
(其值然后传递给bootstrap方法):
public static Comparator<Integer> reference();
0: getstatic #2 // Field COMPARATOR:LComparator;
3: dup
4: invokevirtual #3 // Method Object.getClass:()LClass;
7: pop
8: invokedynamic #4, 0 // InvokeDynamic #0:compare:(LComparator;)LComparator;
13: checkcast #5 // class java/io/Serializable
16: checkcast #6 // class Comparator
19: areturn
缺少间接意味着 COMPARATOR
必须与 lambda 一起序列化。由于 COMPARATOR
不引用 Serializable
值,因此失败。
修复
我不愿将其称为编译器错误(我希望缺少间接寻址可以作为一种优化),尽管这很奇怪。修复是微不足道的,但很难看;在声明中为 COMPARATOR
添加显式转换:
public static final Comparator<Integer> COMPARATOR = (Serializable & Comparator<Integer>) (a, b) -> a > b ? 1 : -1;
这使得在 Java 1.8.0_45 上一切正常。还值得注意的是,eclipse 编译器也在方法引用案例中生成该间接层,因此此 post 中的原始代码不需要修改即可正确执行。
我想补充一个事实,即 lambda 和方法引用之间实际上存在语义差异 实例 方法(即使它们与您的情况具有相同的内容,并且忽略序列化):
SOME_COMPARATOR::compare
此形式计算为一个 lambda 对象,该对象在计算时间 SOME_COMPARATOR
的 值 上封闭(即,它包含对该对象的引用)。它将在评估时间检查SOME_COMPARATOR
是否为空并抛出一个空指针异常。它不会获取在创建后对字段所做的更改。
(a,b) -> SOME_COMPARATOR.compare(a,b)
此表单计算为 lambda 对象,该对象将在调用时访问 SOME_COMPARATOR
字段的值 。它在 this
上关闭,因为 SOME_COMPARATOR
是一个实例字段。调用时它将访问 SOME_COMPARATOR
的当前值并使用它,此时可能会抛出空指针异常。
示范[=46=]
从下面的小例子中可以看出这种行为。通过在调试器中停止代码并检查 lambda 的字段,可以验证它们关闭了什么。
Object o = "First";
void run() {
Supplier<String> ref = o::toString;
Supplier<String> lambda = () -> o.toString();
o = "Second";
System.out.println("Ref: " + ref.get()); // Prints "First"
System.out.println("Lambda: " + lambda.get()); // Prints "Second"
}
Java 语言规范
JLS 在 15.13.3 中描述了方法引用的这种行为:
The target reference is the value of ExpressionName or Primary, as determined when the method reference expression was evaluated.
并且:
First, if the method reference expression begins with an ExpressionName or a Primary, this subexpression is evaluated. If the subexpression evaluates to
null
, aNullPointerException
is raised
在 Tobys 代码中
这可以在 reference
代码的 Tobys 列表中看到,其中 getClass
是在 SOME_COMPARATOR
的值上调用的,如果它为 null 将触发异常:
4: invokevirtual #3 // Method Object.getClass:()LClass;
(或者我认为,我真的不是字节码专家。)
然而,符合 Eclipse 4.4.1 的代码中的方法引用在那种情况下不会抛出异常。 Eclipse 似乎在这里有一个错误。