5

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?

Michael
  • 41,989
  • 11
  • 82
  • 128
  • 1
    why did you reopen the question? It’s an exact duplicate of [that question](https://stackoverflow.com/q/43115645/2711488). – Holger Oct 27 '22 at 15:53
  • 1
    I wanted to add more links, but couldn’t because you already reopened the question. See [Why does javac insert Objects.requireNonNull(this) for final fields?](https://stackoverflow.com/q/62340791/2711488) for a discussion about obsolete null checks inserted by the compiler. The current compiler does not consider when the reference is impossible to be `null` (not even for `this`). – Holger Oct 27 '22 at 16:06
  • 1
    The third question, about `invokedynamic` (do you recognize now, why you shouldn’t ask multiple questions in one?), is unclear. `invokedynamics` does not imply any `null` behavior at all. The code generated by `LamdaMetaFactory` will contain an invocation instruction (`invokevirtual`) that will fail when executed. In contrast, e.g. code generated by `StringConcatFactory` would work without problems with `null` arguments. – Holger Oct 27 '22 at 16:06
  • 1
    The problem is that you asked at least three questions in one. Normally, such questions get closed with “Needs more focus”, precisely to avoid such problems. This would not have happened, if you truly had a asked a question about `invokedynamic` and only about `invokedynamic`. – Holger Oct 27 '22 at 16:12

1 Answers1

4

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.

Johannes Kuhn
  • 14,778
  • 4
  • 49
  • 73
  • Have a couple more for a nice question + nice answer. – Rogue Oct 27 '22 at 15:40
  • 2
    I recommend to run your example with `-XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames` to better demonstrate what’s really going on. – Holger Oct 27 '22 at 16:21