I am learning about reflection in Java. By accident, I discovered the following, for me unexpected behavior.
Both tests as written below succeed.
class NewInstanceUsingReflection {
@Test
void testClassNewInstance()
throws NoSuchMethodException, InvocationTargetException,
InstantiationException, IllegalAccessException
{
final var input = "A string";
final var theClass = input.getClass();
final var constructor = theClass.getConstructor();
final String newString = constructor.newInstance();
assertEquals("", newString);
}
@Test
void testClassNewInstanceWithVarOnly()
throws NoSuchMethodException, InvocationTargetException,
InstantiationException, IllegalAccessException
{
final var input = "A string";
final var theClass = input.getClass();
final var constructor = theClass.getConstructor();
final var newString = constructor.newInstance();
assertEquals("A string", newString);
}
}
The only difference apart from the assertion is that the newString
variable type is explicit in the first test and declared as var
in the second test.
I'm using java 17 and the junit5 test framework.
Why is the value of newString
an empty string in the first test and the input
string value in the second test?
Does it have something todo with the string-pool?
Or is something else going on?
Java17, same problem. The explanation is clearly: bug.
decompiling it, the relevant section:
20: anewarray #2 // class java/lang/Object
23: invokevirtual #35 // Method java/lang/reflect/Constructor.newInstance:([Ljava/lang/Object;)Ljava/lang/Object;
26: checkcast #41 // class java/lang/String
29: astore 4
31: ldc #23 // String A string
33: ldc #23 // String A string
35: invokevirtual #43 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
astore 4
is where the result goes, which is nowhere: slot 4 is not used any further. Instead, the same string constant is loaded twice, trivially resulting in, effectively, "A string".equals("A string")
, which is of course true
.
Replacing var
with String, recompiling, and rerunning javap
:
20: anewarray #2 // class java/lang/Object
23: invokevirtual #35 // Method java/lang/reflect/Constructor.newInstance:([Ljava/lang/Object;)Ljava/lang/Object;
26: checkcast #41 // class java/lang/String
29: astore 4
31: ldc #23 // String A string
33: aload 4
35: invokevirtual #43 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
Identical in every way, except the second ldc
is the correct aload 4
.
I'm having a hard time figuring out what's happening here. It feels more like the var
is somehow causing that ldc
to duplicate (in contrast to an analysis incorrectly thinking that the values are guaranteed to be identical; javac intentionally does very little such optimizations).
I'm having a really hard time figuring out how this has been in 2 LTS releases. Impressive find.
Next step is to verify on the latest JDK (18), and then to file a bug. I did a quick look if it has been reported already, but I'm not sure what search terms to use. I didn't find any report in my search, though.
NB: The decompilation traces were produced using javap -c -v NewInstanceUsingReflection
.
EDIT: Just tried on ecj (Eclipse Compiler for Java(TM) v20210223-0522, 3.25.0, Copyright IBM Corp 2000, 2020. All rights reserved.
) - bug doesn't happen there.