javareflectionstring-pool

Why does String creation using `newInstance()` method behave different when using `var` compared to using explicit type `String`?


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?


Solution

  • 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.