¿Por qué la creación de cadenas usando el método `newInstance()` se comporta de manera diferente cuando se usa `var` en comparación con el tipo explícito `String`?

4 minutos de lectura

avatar de usuario de hanszt
Hanszt

Estoy aprendiendo sobre la reflexión en Java. Por accidente, descubrí lo siguiente, para mí un comportamiento inesperado.

Ambas pruebas, como se escribe a continuación, tienen éxito.

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

La única diferencia aparte de la afirmación es que el newString el tipo de variable es explícito en la primera prueba y se declara como var en la segunda prueba.

Estoy usando java 17 y el marco de prueba junit5.

¿Por qué el valor de newString una cadena vacía en la primera prueba y la input valor de cadena en la segunda prueba?

¿Tiene algo que ver con el string-pool?

¿O está pasando algo más?

  • Extrañamente, en el caso en que newString es varobtienes “Una cadena” solo si input es final.

    – rgettman

    8 sep a las 20:57

  • Mmm… muy extraño… aquí (ideone.com) es una versión de Java 11, sin JUnit assertEquals(...)mostrando el mismo comportamiento.

    – Turing85

    8 sep a las 21:09

  • También se agregó la observación de @rgettman a este caso de prueba.

    – Turing85

    8 sep a las 21:16

  • según el análisis del informe de error, parece que el error es que input tiene un tipo interno como constant string "A string" (lo que ayuda a las optimizaciones) en lugar de solo String. Luego, el compilador calcula que newString es también una instancia de constant string "A string". Escribir String obliga a que sea de tipo String.

    – usuario253751

    9 de septiembre a las 12:29


avatar de usuario de rzwitserlot
rzwitserloot

Java17, mismo problema. La explicación es claramente: error.

descompilándolo, la sección correspondiente:

                20: anewarray #2 // clase java/lang/Object 23: invoquevirtual #35 // Método 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]>esto termina siendo Class<? extends "A string"> (siendo la constante el tipo). El constructor es entonces también Constructor<? extends "A string">y finalmente newInstance devuelve un "A string" como un tipo. Entonces el tipo de newString es la constante, que luego se convierte en un LDC. El error está en cómo el tipo para getClass está determinado.

    – Jorn Vernée

    9 de septiembre a las 17:45


  • @yyyy Con el propósito de ‘qué miembros tiene’, apuesto a que es justo lo que tiene String, que en particular incluye un constructor sin argumentos. El problema es que javac en sí dice: Oh, es una expresión de tipo [voodoo magic here], por lo tanto, puedo cargar directamente desde el grupo constante. En el nivel de bytecode, no ocurre nada especial (javac simplemente cocinó el código de bytes ‘incorrecto’).

    – rzwitserloot

    13 de septiembre a las 18:41

¿Ha sido útil esta solución?