gpt4 book ai didi

java - 运行时级别的 lambda 和方法引用之间有什么区别?

转载 作者:IT老高 更新时间:2023-10-28 20:45:38 24 4
gpt4 key购买 nike

我遇到了使用方法引用而不是 lambdas 发生的问题。该代码如下:

(Comparator<ObjectNode> & Serializable) SOME_COMPARATOR::compare

或者,使用 lambda,
(Comparator<ObjectNode> & Serializable) (a, b) -> SOME_COMPARATOR.compare(a, b)

从语义上讲,它是完全相同的,但实际上它是不同的,因为在第一种情况下,我在其中一个 Java 序列化类中遇到了异常。我的问题不是关于这个异常,因为实际代码运行在一个更复杂的上下文中,已经证明序列化有奇怪的行为,所以如果我提供更多细节,它只会让人难以回答。

我想了解的是这两种创建 lambda 表达式的方法之间的区别。

最佳答案

入门

为了研究这一点,我们从以下类开始:

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$1(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$1:(LInteger;LInteger;)I
#64 (LInteger;LInteger;)I
#65 5
#66 0

我们立即看到 reference() 的字节码方法与 explicit() 的字节码不同.然而,显着的差异 isn't actually relevant ,但引导方法很有趣。

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 的代码由 lambda 使用。 Method arguments下面列出的每个引导方法是作为 LambdaMetaFactory#altMetaFactory 的可变参数(即 args )传递的值.

方法参数的格式
  • samMethodType - 函数对象要实现的方法的签名和返回类型。
  • implMethod - 描述应该在调用时调用的实现方法的直接方法句柄(适当调整参数类型、返回类型,并在调用参数前添加捕获的参数)。
  • instantiatedMethodType - 应该在调用时动态强制执行的签名和返回类型。这可能与 samMethodType 相同,也可能是它的特化。
  • flags 表示附加选项;这是所需标志的按位或。定义的标志是 FLAG_BRIDGES、FLAG_MARKERS 和 FLAG_SERIALIZABLE。
  • bridgeCount 是函数对象应该实现的附加方法签名的数量,并且当且仅当设置了 FLAG_BRIDGES 标志时才存在。

  • 在这两种情况下 bridgeCount是 0,所以没有 6,否则就是 bridges - 要实现的附加方法签名的可变长度列表(假设 bridgeCount 为 0,我不完全确定为什么设置 FLAG_BRIDGES)。

    将上述内容与我们的论点相匹配,我们得到:
  • 函数签名和返回类型 (Ljava/lang/Object;Ljava/lang/Object;)I ,这是 Comparator#compare 的返回类型, 因为泛型类型删除。
  • 调用此 lambda 时调用的方法(不同)。
  • lambda 的签名和返回类型,在调用 lambda 时会检查:(LInteger;LInteger;)I (请注意,这些不会被删除,因为这是 lambda 规范的一部分)。
  • 标志,在这两种情况下都是由 FLAG_BRIDGES 组成的和 FLAG_SERIALIZABLE (即 5)。
  • 桥接方法签名的数量,0。

  • 我们可以看到为两个 lambda 设置了 FLAG_SERIALIZABLE,所以不是这样。

    实现方法

    方法引用 lambda 的实现方法是 Comparator.compare:(LObject;LObject;)I ,但对于显式 lambda,它是 Generic.lambda$explicit$df5d232f$1:(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)

    其中重要的部分是“捕获的参数”。回顾反汇编的字节码,方法引用的invokedynamic指令看起来确实像是在捕获一个比较器( #0:compare:(LComparator;)LComparator; ,与显式的 lambda 形成对比, #1:compare:()LComparator; )。

    确认捕获是问题
    ObjectOutputStream包含 extendedDebugInfo字段,我们可以使用 -Dsun.io.serialization.extendedDebugInfo=true 设置虚拟机参数:

    $ java -Dsun.io.serialization.extendedDebugInfo=true Generic



    当我们再次尝试序列化 lambdas 时,这给出了一个非常令人满意的结果
    Exception in thread "main" java.io.NotSerializableException: Generic$$Lambda$1/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$1(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$1 ,反过来调用 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 编译器也在方法引用案例中生成了该间接层,因此本文中的原始代码不需要修改即可正确执行。

    关于java - 运行时级别的 lambda 和方法引用之间有什么区别?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30514995/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com