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

标签 java serialization lambda java-8

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

(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,但是引导方法很有趣。


  invokedynamic调用站点通过引导程序方法链接到方法,该方法是由编译器为动态类型语言指定的一种方法,该方法由JVM调用一次以链接该站点。


Java Virtual Machine Support for Non-Java Languages,强调他们的)

这是负责创建lambda使用的CallSite的代码。每个引导方法下方列出的Method arguments是作为LambdaMetaFactory#altMetaFactory的可变参数(即args)传递的值。

方法参数的格式


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


在这两种情况下,bridgeCount均为0,因此不存在6,否则为bridges-要实现的其他方法签名的变长列表(假设bridgeCount为0,我并不完全是确定为何设置FLAG_BRIDGES)。

将以上内容与我们的论据相匹配,我们得到:


由于通用类型擦除,函数签名和返回类型(Ljava/lang/Object;Ljava/lang/Object;)IComparator#compare的返回类型。
调用此lambda时所调用的方法(不同)。
lambda的签名和返回类型,将在调用lambda时进行检查:(LInteger;LInteger;)I(请注意,不会擦除它们,因为这是lambda规范的一部分)。
标志,在两种情况下都是FLAG_BRIDGESFLAG_SERIALIZABLE的组成(即5)。
桥接方法签名的数量,0。


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

实施方法

方法参考lambda的实现方法为Comparator.compare:(LObject;LObject;)I,而显式lambda的实现方法为Generic.lambda$explicit$df5d232f$1:(LInteger;LInteger;)I。查看反汇编,我们可以看到前者本质上是后者的内联版本。唯一的其他显着差异是方法参数类型(如前所述,这是由于通用类型擦除)。

Lambda何时可序列化?


  如果lambda表达式的目标类型和捕获的参数可序列化,则可以对其进行序列化。


Lambda Expressions (The Java™ Tutorials)

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

确认捕获是问题

ObjectOutputStream包含一个extendedDebugInfo字段,我们可以使用-Dsun.io.serialization.extendedDebugInfo=true VM参数设置该字段:


  $ java -Dsun.io.serialization.extendedDebugInfo = true通用


当我们尝试再次序列化lambda时,这给出了非常令人满意的结果

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/52132046/

相关文章:

java - 将对象保存到文件中,并在以后向其中添加更多对象

python - Python 中的 Lambda 和多语句

java - Oracle 与 JDBC 的连接

Java浅拷贝

java - 如何深度复制具有初始容量的 map ?

Java 8 lambda : is automatically-inferred exception a RuntimeException by default?

lambda - 方案:β 降低挑战

java.lang.NoClassDefFoundError : okhttp3. OkHttpClient$Builder

Java/安卓 : Continuously rotate ImageView image on tap of left/right side of screen

javascript - 使用 jQuery 将表单数据转换为 JavaScript 对象