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?