当前位置: 首页 > 知识库问答 >
问题:

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

袁山
2023-03-14

我在使用方法引用时遇到了一个问题,但在使用lambdas时没有遇到。代码如下:

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

或者,与lambda,

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

在语义上,它是严格相同的,但在实践中它是不同的,因为在第一种情况下,我在一个Java序列化类中得到了一个异常。我的问题不是关于这个异常,因为实际的代码运行在一个更复杂的上下文中,这个上下文已被证明具有奇怪的序列化行为,所以如果我提供更多细节,那么回答这个问题就太困难了。

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

共有2个答案

凌修伟
2023-03-14

我想补充一个事实,lambda和引用实例方法的方法之间实际上存在语义差异(即使它们的内容与您的情况相同,并且不考虑序列化):

此窗体计算为lambda对象,该对象在计算时在SOME_COMPARATOR的值上闭合(也就是说,它包含对该对象的引用)。它将检查某个_COMPARATOR在计算时是否为null,然后抛出null指针异常。它不会拾取在创建字段后对字段所做的更改。

这个表单的计算结果是一个lambda对象,它将在调用时访问SOME_COMPARATOR字段的值。它被关闭在这个上,因为某个比较器是一个实例字段。调用时,它将访问某个_COMPARATOR的当前值并使用该值,此时可能会引发空指针异常。

从下面的小例子可以看出这种行为。通过在调试器中停止代码并检查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"
}

JLS在15.13.3中描述了方法参考的这种行为:

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

和:

首先,如果方法引用表达式以ExpressionName或主表达式开头,则计算此子表达式。如果子表达式计算为null,则引发一个NullPointerExc0019

这可以在Tobys列出的reference代码中看到,其中getClass是对SOME_COMPARATOR的值调用的,如果它为空,就会触发异常:

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

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

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

昌博易
2023-03-14

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

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通用。班

删除不相关的部分(以及一些其他杂乱的内容,比如完全限定的类型和比较器的初始化),我们就剩下了

  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()方法的字节码不同。然而,显著的差异实际上并不相关,但是引导方法很有趣。

invokedynamic调用站点通过bootstrap方法链接到一个方法,bootstrap方法是编译器为动态类型语言指定的方法,JVM会调用该语言一次以链接站点。

(Java虚拟机对非Java语言的支持,特别强调)

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

  1. samMethodType——函数对象要实现的方法的签名和返回类型

在这两种情况下,bridgeCount都是0,因此没有6,否则它将是bridges——一个可变长度的附加方法签名列表,以实现(鉴于bridgeCount是0,我不完全确定为什么要设置FLAG_bridges)。

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

  1. 函数签名和返回类型(Ljava/lang/Object; Ljava/lang/Object;)I,这是比较器#比较的返回类型,因为泛型类型擦除。
  2. 调用此lambda时调用的方法(这是不同的)。
  3. lambda的签名和返回类型,当调用lambda时将检查:(Lintger; Lintger;)I(请注意,这些不会被擦除,因为这是lambda规范的一部分)。
  4. 标志,在这两种情况下都是FLAG_BRIDGES和FLAG_SERIALIZABLE(即。5)。
  5. 桥方法签名量,0.

我们可以看到FLAG_SERIALIZABLE是为两个lambdas设置的,所以不是那样的。

方法参考lambda的实现方法是Comparator。比较:(LObject;LObject;)I,但对于显式lambda,它是通用的。lambda$explicit$df5d232f$1:(林特格;林特格;)我。查看反汇编,我们可以看到前者本质上是后者的内联版本。唯一其他显著的区别是方法参数类型(如前所述,这是因为泛型类型擦除)。

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

Lambda表达式(Java™教程)

其中重要的部分是“捕获的论点”。回过头来看分解后的字节码,方法引用的invokedynamic指令看起来确实像是捕获了一个比较器(#0:比较:(L比较器;)L比较器;,与显式lambda相反,#1:比较:()L比较器;)。

ObjectOutputStream包含一个extendedDebugInfo字段,我们可以使用-Dsun设置该字段。伊奥。序列化。extendedDebugInfo=trueVM参数:

$java-Dsun。伊奥。序列化。extendedDebugInfo=真正的泛型

当我们再次尝试序列化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,这反过来又调用了比较器#compare。这个间接层意味着它不会捕获任何不可序列化的内容(或者准确地说是任何内容),因此可以安全地进行序列化。方法引用表达式直接使用比较器(然后将其值传递给引导方法):

  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

缺少间接寻址意味着比较器必须与lambda一起序列化。由于比较器未引用可序列化的值,因此此操作失败。

我不太愿意称这为编译器错误(我认为缺乏间接性是一种优化),尽管这很奇怪。修复方法很琐碎,但很丑陋;在声明处为COMPARATOR添加显式强制转换:

public static final Comparator<Integer> COMPARATOR = (Serializable & Comparator<Integer>) (a, b) -> a > b ? 1 : -1;

这使得一切都能在Java 1.8.045上正确执行。还值得注意的是,eclipse编译器在方法引用案例中也会生成该间接层,因此本文中的原始代码不需要修改就可以正确执行。

 类似资料:
  • 问题内容: 我遇到了使用方法引用而不是lambda发生的问题。该代码如下: 或者,用lambda 从语义上讲,它是严格相同的,但实际上与第一种情况不同,我在一个Java序列化类中遇到了一个异常。我的问题不是关于此异常的问题,因为实际的代码正在更复杂的上下文中运行,事实证明该序列化具有奇怪的行为,因此如果我提供更多详细信息,这将使回答变得非常困难。 我想了解的是这两种创建lambda表达式的方式之间

  • 问题内容: 斯威夫特有: 强引用 参考文献薄弱 无人参考 无主引用与弱引用有何不同? 什么时候可以使用无主引用安全? 无主引用是否像C / C ++中的悬空指针一样具有安全风险? 问题答案: 双方并引用不创建一个被引用的对象上保持(又名它们不会取消分配引用的对象增加,为了保留计数,以防止电弧)。 但是为什么要两个关键词呢?这种区别与类型内置在Swift语言中这一事实有关。长话短说:可选类型提供了内

  • 问题内容: 在此示例中: 无法编译为: 而被编译器接受。 这个答案说明唯一的区别是,与不同,它允许您稍后引用类型,似乎并非如此。 是什么区别,并在这种情况下,为什么不第一编译? 问题答案: 通过使用以下签名定义方法: 并像这样调用它: 在jls§8.1.2中,我们发现(有趣的部分被我加粗了): 通用类声明定义了一组参数化类型(第4.5节), 每种可能通过类型arguments调用类型参数节的类型

  • 问题内容: 将一个使用在另一个上是否有好处?在Python 2中,它们似乎都返回相同的结果: 问题答案: 在将返回2.5并且将返回2。前者是浮点除法,后者是地板除法,有时也称为整数除法。 在或更高版本的2.x行中,除非执行,否则整数没有区别,这会使采取3.0的行为。 不管将来的进口是什么,都会归还,2.0因为这是操作的地板分割结果。

  • 和有什么区别?他们彼此有关系吗?或者它们只是并发实现? 是否有人与他们一起工作,并能给出/解释两者的利弊? 使用我指的是ng-bootstrap.github和 与我的意思是valor-software-ngx-bootstrap。 两者都与Angular 4有关(不是AngularJS!)和引导4。 请注意,这不是一个重复的问题ngx-bootstrap和ng2 bootstrap之间的区别?。

  • 问题内容: 我对python级别的函数和常规函数(用定义)之间的差异感到好奇。(我知道对程序员有什么区别,以及何时使用每个程序员。) 如我们所见-python 知道 这是一个函数,并且是一个常规函数。这是为什么?它们 与python有 什么区别? 问题答案: 它们是同一类型,因此它们的处理方式相同: Python还知道将其定义为lambda函数,并将其设置为函数名称: 换句话说,它影响了该函数将获