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?

hanszt
  • 583
  • 1
  • 8
  • Strangely, in the case where `newString` is `var`, you get "A string" only if `input` is `final`. – rgettman Sep 08 '22 at 20:57
  • Hmm... very strange... [here (`ideone.com`)](https://ideone.com/yr5wgA) is a Java 11 version, without JUnit's `assertEquals(...)`, showing the same behaviour. – Turing85 Sep 08 '22 at 21:09
  • Also added @rgettman's observation to this test case. – Turing85 Sep 08 '22 at 21:16
  • 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. – user253751 Sep 09 '22 at 12:29

1 Answers1

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.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • 1
    reproduced with Java 19 (OpenJDK) - not with Eclipse – user16320675 Sep 08 '22 at 21:40
  • 1
    `final var newString = "A string".getClass().getConstructor().newInstance();` yields the same result. Good find. – OscarRyz Sep 08 '22 at 21:43
  • 7
    FTR: I have submitted a bug report to oracle. Will post the link to the issue as soon as I get it. – Turing85 Sep 08 '22 at 21:48
  • 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. – hanszt Sep 09 '22 at 06:58
  • Reproduced with Java 18 (Oracle OpenJDK version 18) – hanszt Sep 09 '22 at 07:16
  • 7
    Update: [Here is the bug-report (ID: JDK-8293578)](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8293578) – Turing85 Sep 09 '22 at 07:55
  • 4
    Link for those who prefer reading JDK tickets in a JIRA interface: https://bugs.openjdk.org/browse/JDK-8293578 – andrybak Sep 09 '22 at 08:42
  • 7
    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. – Jorn Vernee Sep 09 '22 at 17:45
  • @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. – yyyy Sep 13 '22 at 17:45
  • 1
    also I found that `var newString = "foo".getClass().getConstructor().newInstance(); newString = "bar"; println(newString);` prints `foo`. Are there something wrong with the constant propagation? – yyyy Sep 13 '22 at 17:48
  • 2
    @yyyy 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). – rzwitserloot Sep 13 '22 at 18:41
  • 1
    @yyyy 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. – rzwitserloot Sep 13 '22 at 18:42
  • @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?) – Lii Sep 14 '22 at 07:19