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?
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 NullPointerException
s - 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.