Warum verhält sich die String-Erstellung mit der Methode „newInstance()“ bei der Verwendung von „var“ anders als bei der Verwendung des expliziten Typs „String“?

Lesezeit: 4 Minuten

Benutzeravatar von hanszt
hanszt

Ich lerne über Reflektion in Java. Durch Zufall entdeckte ich folgendes, für mich unerwartetes Verhalten.

Beide unten beschriebenen Tests sind erfolgreich.

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);
    }
}

Der einzige Unterschied abgesehen von der Behauptung ist, dass die newString Variablentyp ist im ersten Test explizit und deklariert als var im zweiten Test.

Ich verwende Java 17 und das Testframework junit5.

Warum ist der Wert von newString eine leere Zeichenfolge im ersten Test und die input Zeichenfolgenwert im zweiten Test?

Hat es etwas mit dem String-Pool zu tun?

Oder geht noch was?

  • Seltsamerweise in dem Fall, wo newString ist varerhalten Sie “A string” nur dann, wenn input ist final.

    – rgettmann

    8. September um 20:57 Uhr

  • Hm…sehr seltsam… hier (ideone.com) ist eine Java 11-Version ohne JUnit’s assertEquals(...)zeigt das gleiche Verhalten.

    – Turing85

    8. September um 21:09 Uhr

  • Außerdem wurde diesem Testfall die Beobachtung von @rgettman hinzugefügt.

    – Turing85

    8. September um 21:16 Uhr

  • Laut der Analyse des Fehlerberichts klingt es so, als ob der Fehler das ist input hat einen internen Typ wie constant string "A string" (was Optimierungen hilft) statt nur String. Dann berechnet der Compiler das newString ist auch eine Instanz von constant string "A string". Das Schreiben von String erzwingt den Typ String.

    – Benutzer253751

    9. September um 12:29 Uhr


Benutzeravatar von rzwitserloot
rzwitserloot

Java17, gleiches Problem. Die Erklärung ist eindeutig: Fehler.

dekompilieren, der relevante Abschnitt:

                20: anewarray #2 // Klasse java/lang/Object 23: invokevirtual #35 // Methode 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.

  • FTR: I have submitted a bug report to oracle. Will post the link to the issue as soon as I get it.

    – Turing85

    Sep 8 at 21:48

  • Update: Here is the bug-report (ID: JDK-8293578)

    – Turing85

    Sep 9 at 7:55


  • Link for those who prefer reading JDK tickets in a JIRA interface: bugs.openjdk.org/browse/JDK-8293578

    – andrybak

    Sep 9 at 8:42

  • 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]>das ist am Ende Class<? extends "A string"> (die Konstante ist der Typ). Der Konstruktor ist dann auch Constructor<? extends "A string">und schlussendlich newInstance gibt a zurück "A string" als Typ. Also die Art von newString ist die Konstante, die dann in einen LDC umgewandelt wird. Der Fehler liegt darin, wie der Typ für getClass festgestellt wird.

    – Jorn Vernee

    9. September um 17:45 Uhr


  • @yyyy Für den Zweck, „welche Mitglieder haben Sie“, wette ich, dass es genau das ist, was String hat, was insbesondere einen No-Args-Konstruktor enthält. Das Problem ist, dass javac selbst sagt: Oh, es ist ein Ausdruck des Typs [voodoo magic here], daher kann ich einfach direkt aus dem konstanten Pool laden. Auf Bytecode-Ebene passiert nichts Besonderes (javac einfach den ‘falschen’ Bytecode zusammengebraut).

    – rzwitserloot

    13. September um 18:41 Uhr

1424430cookie-checkWarum verhält sich die String-Erstellung mit der Methode „newInstance()“ bei der Verwendung von „var“ anders als bei der Verwendung des expliziten Typs „String“?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy