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?
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.
Seltsamerweise in dem Fall, wo
newString
istvar
erhalten Sie “A string” nur dann, wenninput
istfinal
.– rgettmann
8. September um 20:57 Uhr
Hm…sehr seltsam… hier (
ideone.com
) ist eine Java 11-Version ohne JUnit’sassertEquals(...)
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 wieconstant string "A string"
(was Optimierungen hilft) statt nurString
. Dann berechnet der Compiler dasnewString
ist auch eine Instanz vonconstant string "A string"
. Das Schreiben von String erzwingt den Typ String.– Benutzer253751
9. September um 12:29 Uhr