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

时间:2015-05-28 18:59:04

标签: java serialization lambda java-8

我遇到过使用方法引用但没有使用lambdas的问题。该代码如下:

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

或者,使用lambda,

(Comparator<ObjectNode> & Serializable) (a, b) -> SOME_COMPARATOR.compare(a, b)

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

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

2 个答案:

答案 0 :(得分:27)

入门

为了调查这一点,我们从以下课程开始:

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)传递的值。

Method参数的格式

  1. samMethodType - 函数对象要实现的签名和返回方法类型。
  2. implMethod - 一个直接方法句柄,用于描述应该在调用时调用的实现方法(通过适当调整参数类型,返回类型以及在调用参数之前使用捕获的参数)。
  3. instantiatedMethodType - 应在调用时动态强制执行的签名和返回类型。这可能与samMethodType相同,也可能是它的特化。
  4. 标志表示其他选项;这是所需标志的按位OR。定义的标志是FLAG_BRIDGES,FLAG_MARKERS和FLAG_SERIALIZABLE。
  5. bridgeCount是函数对象应实现的附加方法签名的数量,当且仅当设置了FLAG_BRIDGES标志时才存在。
  6. 在这两种情况下,bridgeCount都为0,因此没有6,否则为bridges - 要实现的其他方法签名的可变长度列表(假设为bridgeCount }是0,我不完全确定为什么设置了FLAG_BRIDGES。

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

    1. 函数签名和返回类型(Ljava/lang/Object;Ljava/lang/Object;)I,它是Comparator#compare的返回类型,因为通用类型擦除。
    2. 调用此lambda时调用的方法(不同)。
    3. lambda的签名和返回类型,将在调用lambda时检查:(LInteger;LInteger;)I(请注意,这些都不会被删除,因为这是lambda规范的一部分)。
    4. 旗帜,在两种情况下都是FLAG_BRIDGESFLAG_SERIALIZABLE的组成(即5)。
    5. 桥接方法签名的数量,0。
    6. 我们可以看到为两个lambdas设置了FLAG_SERIALIZABLE,所以不是这样。

      实施方法

      方法参考lambda的实现方法是Comparator.compare:(LObject;LObject;)I,但对于显式lambda,它是Generic.lambda$explicit$df5d232f$1:(LInteger;LInteger;)I。看一下反汇编,我们可以看到前者本质上是后者的内联版本。另一个值得注意的区别是方法参数类型(如前所述,这是因为泛型类型擦除)。

      lambda实际上何时可序列化?

        

      如果lambda表达式的目标类型及其捕获的参数是可序列化的,则可以序列化它。

      Lambda Expressions (The Java™ Tutorials)

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

      确认捕获是问题

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

        

      $ 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编译器也在方法引用案例中产生了间接层,因此本文中的原始代码不需要修改即可正确执行。

答案 1 :(得分:13)

我想补充一个事实: lambda和方法引用之间实际上存在实例方法的语义差异(即使它们具有相同的内容)在你的情况下,并忽略序列化):

SOME_COMPARATOR::compare

此表单计算为lambda对象,该对象在评估时的SOME_COMPARATOR 上关闭(即,它包含对该对象的引用) 。它将在评估时检查SOME_COMPARATOR是否为 ,然后抛出空指针异常。它不会获取创建后对字段所做的更改。

(a,b) -> SOME_COMPARATOR.compare(a,b)

此表单计算为lambda对象,该对象将在调用时访问SOME_COMPARATOR字段的值。它已关闭this,因为SOME_COMPARATOR是一个实例字段。调用时,它将访问当前值SOME_COMPARATOR并使用它,可能会在此时抛出空指针异常。

示范

从以下小例子可以看出这种行为。通过在调试器中停止代码并检查lambdas的字段,可以验证它们被关闭的内容。

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中方法引用的这种行为:

  

目标引用是ExpressionName或Primary的值,在评估方法引用表达式时确定。

  

首先,如果方法引用表达式以ExpressionName或Primary开头,则计算此子表达式。如果子表达式的计算结果为null,则会引发NullPointerException

在Tobys代码中

这可以在Tobys列出的reference代码中看到,其中getClassSOME_COMPARATOR的值上调用,如果它为null,将触发异常:

4: invokevirtual #3   // Method Object.getClass:()LClass;

(或者我认为,我真的不是字节码的专家。)

符合Eclipse 4.4.1的代码中的方法引用在这种情况下不会抛出异常。 Eclipse似乎有一个错误。