为方法引用生成的 getClass 调用的值是多少?

Mic*_*ael 5 java java-8 jvm-bytecode

我有以下人为的代码示例。为了保持字节码较小,它没有做任何有用的事情,但希望您可以看到,通过一些更改,它会如何发挥作用。

List<String> letters = Arrays.asList("a", "b");
Stream.of(/*a, b, c, d*/).filter(letters::contains).toArray(String[]::new);
Run Code Online (Sandbox Code Playgroud)

Java 8 生成以下字节码

  public Main();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=2, args_size=1
        start local 0 // Main this
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: iconst_2
         5: anewarray     #2                  // class java/lang/String
         8: dup
         9: iconst_0
        10: ldc           #3                  // String a
        12: aastore
        13: dup
        14: iconst_1
        15: ldc           #4                  // String b
        17: aastore
        18: invokestatic  #5                  // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
        21: astore_1
        start local 1 // java.util.List letters
        22: iconst_0
        23: anewarray     #6                  // class java/lang/Object
        26: invokestatic  #7                  // InterfaceMethod java/util/stream/Stream.of:([Ljava/lang/Object;)Ljava/util/stream/Stream;
        29: aload_1
        30: dup
        31: invokevirtual #8                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
        34: pop
        35: invokedynamic #9,  0              // InvokeDynamic #0:test:(Ljava/util/List;)Ljava/util/function/Predicate;
        40: invokeinterface #10,  2           // InterfaceMethod java/util/stream/Stream.filter:(Ljava/util/function/Predicate;)Ljava/util/stream/Stream;
        45: invokedynamic #11,  0             // InvokeDynamic #1:apply:()Ljava/util/function/IntFunction;
        50: invokeinterface #12,  2           // InterfaceMethod java/util/stream/Stream.toArray:(Ljava/util/function/IntFunction;)[Ljava/lang/Object;
        55: pop
        56: return
        end local 1 // java.util.List letters
        end local 0 // Main this
Run Code Online (Sandbox Code Playgroud)

我对这一点特别感兴趣

30: dup
31: invokevirtual #8 // Method java/lang/Object.getClass:()Ljava/lang/Class;
34: pop
Run Code Online (Sandbox Code Playgroud)

这实际上相当于将代码更改为

List<String> letters = Arrays.asList("a", "b");
letters.getClass(); // inserted
Stream.of().filter(letters::contains).toArray(String[]::new);
Run Code Online (Sandbox Code Playgroud)

在 Java 9+ 中,这已更改为对Objects.requireNonNull.

30: dup
31: invokestatic  #8 // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;
34: pop
Run Code Online (Sandbox Code Playgroud)

我想我明白了这两点的要点:如果方法引用引用的变量为 null,则生成 NullPointerException。如果letters为 null,则调用getClass()它会抛出异常,从而使下一次取消引用变得安全。

根据文档invokedynamic(用于调用contains)本身不能抛出 NPE:“总的来说,这些不变量意味着绑定到调用站点对象的 invokedynamic 指令永远不会抛出 NullPointerException”,因此编译器可能会抛出 NullPointerException 是有道理的预先插入提供该保证的其他内容。

但在这种情况下,该变量实际上是最终变量,并且包含构造函数调用的结果。我相信它保证非空。对这种情况跳过此检查可能只是不存在的编译器优化,还是我错过了一些边缘情况?

我要求的是一个具体的、实际的原因。我正在使用 AspectJ 来编写 javac 的字节码,并且 AspectJ 似乎正在“优化”这 3 个指令,我猜是因为它认为它们不执行任何操作。这个项目使用的是Java 8。我没有检查它是否被删除为9+。

在我上面展示的情况下,也许删除是可以的,因为引用不能为空,但我在我们的代码库中看到了数百种这种情况发生的情况,并且很难彻底证明它们都是安全的。

invokedynamic如果引用null,由于 AspectJ 破坏字节码,会出现什么行为?不明确的?

Joh*_*uhn 5

事实上,Object.getClass()被用来发出NullPointerException.
较新的 Java 版本使用Objects.requireNonNull

我稍微减少了再现器,并添加了另一种方法来显示差异:

import java.util.List;
import java.util.function.Predicate;

public class LambdaTest {
    public static Predicate<String> test1ref(List<String> letters) {
        return letters::contains;
    }
    
    public static Predicate<String> test2lambda(List<String> letters) {
        return s -> letters.contains(s);
    }
}
Run Code Online (Sandbox Code Playgroud)

使用 Java 17 进行编译时,javap -p -c LambdaTest输出以下内容:

Compiled from "LambdaTest.java"
public class LambdaTest {
  public LambdaTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static java.util.function.Predicate<java.lang.String> test1ref(java.util.List<java.lang.String>);
    Code:
       0: aload_0
       1: dup
       2: invokestatic  #7                  // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;
       5: pop
       6: invokedynamic #13,  0             // InvokeDynamic #0:test:(Ljava/util/List;)Ljava/util/function/Predicate;
      11: areturn

  public static java.util.function.Predicate<java.lang.String> test2lambda(java.util.List<java.lang.String>);
    Code:
       0: aload_0
       1: invokedynamic #17,  0             // InvokeDynamic #1:test:(Ljava/util/List;)Ljava/util/function/Predicate;
       6: areturn

  private static boolean lambda$test2lambda$0(java.util.List, java.lang.String);
    Code:
       0: aload_0
       1: aload_1
       2: invokeinterface #18,  2           // InterfaceMethod java/util/List.contains:(Ljava/lang/Object;)Z
       7: ireturn
}
Run Code Online (Sandbox Code Playgroud)

两种方法的行为相似 - 一种使用直接引用,另一种被脱糖为私有方法 - 这在这里并不重要。

如果test2lambda使用null作为参数调用,它将成功创建 lambda - 并且只有NullPointerException当有人调用Predicatetest()方法时才会失败。
相反,test1ref会提前失败 - 因为绑定到null对象并不是真正有用。

但它的行为类似于test2lambda如果null省略检查 - “成功”创建一个Predicate只会抛出NullPointerExceptions - 尽管堆栈跟踪指向 lambda 的使用站点。

我们可以通过自己创建这样一个 lambda 来测试这一点:

import java.lang.invoke.*;
import static java.lang.invoke.MethodType.methodType;
import java.util.List;
import java.util.function.Predicate;

public class DirectRef {
    public static void main(String[] args) throws Throwable {
        MethodHandles.Lookup l = MethodHandles.lookup();
        MethodHandle target = l.findVirtual(List.class, "contains", methodType(boolean.class, Object.class));
        CallSite cs = LambdaMetafactory.metafactory(l, "test", methodType(Predicate.class, List.class),
                methodType(boolean.class, Object.class), target, methodType(boolean.class, String.class));
        @SuppressWarnings("unchecked")
        Predicate<String> pred = (Predicate<String>) cs.dynamicInvoker().invokeExact((List<?>) null);
        pred.test("foo"); // Line 14
    }
}
Run Code Online (Sandbox Code Playgroud)

运行此程序时,我收到以下异常:

Exception in thread "main" java.lang.NullPointerException
        at DirectRef.main(DirectRef.java:14)
Run Code Online (Sandbox Code Playgroud)

事实上,它指向谓词的使用地点。

  • 我建议使用“-XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames”运行您的示例,以更好地演示到底发生了什么。 (2认同)