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

1

71

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?

Sixtieth answered 8/9, 2022 at 20:39 Comment(4)
Strangely, in the case where newString is var, you get "A string" only if input is final.Quadragesimal
Hmm... very strange... here (ideone.com) is a Java 11 version, without JUnit's assertEquals(...), showing the same behaviour.Caton
Also added @rgettman's observation to this test case.Caton
according to the bug report analysis, it sounds like the bug is that input has an internal type like constant string "A string" (which helps optimizations) instead of just String. Then, the compiler figures that newString is also an instance of constant string "A string". Writing String forces it to be String type.Versed
B
43

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.

Bessiebessy answered 8/9, 2022 at 21:22 Comment(13)
reproduced with Java 19 (OpenJDK) - not with EclipseErechtheus
final var newString = "A string".getClass().getConstructor().newInstance(); yields the same result. Good find.Robbert
FTR: I have submitted a bug report to oracle. Will post the link to the issue as soon as I get it.Caton
Good to see that this question sparks a good discussion and that it is indeed identified as a bug by the community. Thanks for submitting it to oracle Turing85 and for the analysis using decompilation rzwitserloot. I'm curious about the follow-up of the issue.Sixtieth
Reproduced with Java 18 (Oracle OpenJDK version 18)Sixtieth
Update: Here is the bug-report (ID: JDK-8293578)Caton
Link for those who prefer reading JDK tickets in a JIRA interface: bugs.openjdk.org/browse/JDK-8293578Customary
As for what causes this (I think...): constants are represented in javac as types (just like they are in many other compilers). Since getClass() returns Class<? extends [exact type]>, this ends up being Class<? extends "A string"> (the constant being the type). The constructor is then also Constructor<? extends "A string">, and finally newInstance returns a "A string" as a type. So the type of newString is the constant, which then gets turned into an LDC. The bug being in how the type for getClass is determined.Politic
@JornVernee But why does Class<"string"> have a no-arg constructor? If all const types has a no-arg constructor by default, then Class<42> should also have one, but it doesn't and throws NoSuchMethodException. If (not likely) it inherited the constructor from String.class, then this is also a bug.Throng
also I found that var newString = "foo".getClass().getConstructor().newInstance(); newString = "bar"; println(newString); prints foo. Are there something wrong with the constant propagation?Throng
@Throng For the purpose of 'which members do you have', I bet it's just what String has, which notably includes a no-args constructor. The problem is that javac itself goes: Oh, it's an expression of type [voodoo magic here], therefore I can just load straight from the constant pool. At the bytecode level nothing special is going on (javac simply cooked up the 'wrong' bytecode).Bessiebessy
@Throng Same problem. newString is of type "foo constant" (which is nowhere in the spec, its a 'trick' employed by javac, which is being incorrectly applied here), and thus println(newString) is being turned into bytecode by pulling "foo" out of the constant pool, instead of passing the actual variable.Bessiebessy
@Turing85: The bug report says this: "EXPECTED - The call on line 20 ("".equals(newString)) should evaluates to "false"." Surely that is not correct!? newString should have value "", since it should be created with the 0-arg String constructor. Did you mix up the "expected" and "actual" sections in the bug report? (Or am I confused?)Myrtismyrtle

© 2022 - 2024 — McMap. All rights reserved.