Search code examples
javajava-8jvm-bytecode

What's the value of the generated getClass invocation for method references?


I have the following contrived code example. It does nothing useful, in order to keep the bytecode small, but hopefully you can see how, with some changes, it might.

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

Java 8 generates the following bytecode

  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

I'm specifically interested in this bit

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

This is effectively equivalent to changing the code to

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

In Java 9+, this has changed to a call to Objects.requireNonNull.

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

I think I see the point of both of these: to generate a NullPointerException if the variable referred to by the method reference is null. If letters is null, calling getClass() on it will throw, making the next dereference safe.

According to the docs, invokedynamic (which is used to call contains) cannot throw a NPE itself: "Together, these invariants mean that an invokedynamic instruction which is bound to a call site object never throws a NullPointerException", so it makes sense that the compiler might insert something else which provides that guarantee beforehand.

In this case, though, the variable is effectively final and contains the result of a constructor invocation. I believe it's guaranteed non-null. Could skipping this check for such cases just be a compiler optimization that doesn't exist, or am I missing some edge case?

I'm asking for a specific, practical reason. I'm using AspectJ to weave javac's bytecode, and AspectJ seems to be "optimizing away" those 3 instructions, I presume because it thinks they don't do anything. This project is using Java 8. I didn't check whether it's erased for 9+.

In the case I've shown above, maybe that removal is fine since the reference cannot be null, but I see hundreds of cases where this happens in our codebase and it will be difficult to exhaustively prove they're all safe.

What would be the behaviour of invokedynamic if the reference was null, through consequence of AspectJ mangling the bytecode? Undefined?


Solution

  • Indeed, Object.getClass() was used to emit a NullPointerException.
    Newer Java versions use Objects.requireNonNull

    I reduced the reproducer a bit, and added an other method to show the difference:

    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);
        }
    }
    

    When compiling that with Java 17, javap -p -c LambdaTest outputs the following:

    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
    }
    

    Both methods behave similar - one using a direct reference, the other is desugared into a private method - which doesn't matter here.

    If test2lambda is called with null as argument, it will successfully create the lambda - and only fails with a NullPointerException when someone calls the Predicate's test() method.
    In contrast, test1ref will fail early - as binding to a null object is not really useful.

    But it would behave similarly to test2lambda if the null check is omitted - "successfully" creating a Predicate that will only throw NullPointerExceptions - although the stack trace points to the use-site of the lambda instead.

    We can test this by creating such a lambda our self:

    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
        }
    }
    

    When running this, I get the following exception:

    Exception in thread "main" java.lang.NullPointerException
            at DirectRef.main(DirectRef.java:14)
    

    Indeed, it is pointing at the use site of the predicate.