Pregunta Eficiencia de Java "Inicialización doble de llaves"?


En Características ocultas de Java la respuesta superior menciona Inicialización de Double Brace, con un muy sintaxis atractiva

Set<String> flavors = new HashSet<String>() {{
    add("vanilla");
    add("strawberry");
    add("chocolate");
    add("butter pecan");
}};

Este modismo crea una clase interna anónima con solo un inicializador de instancia, que "puede usar cualquier [...] método en el ámbito que lo contiene".

Pregunta principal: ¿Esto es así? ineficiente como suena? ¿Debería limitarse su uso a inicializaciones únicas? (¡Y por supuesto presumiendo!)

Segunda pregunta: el nuevo HashSet debe ser el "esto" utilizado en el inicializador de instancias ... ¿alguien puede arrojar luz sobre el mecanismo?

Tercera pregunta: ¿Es este idioma también? oscuro para usar en el código de producción?

Resumen: Muy, muy buenas respuestas, gracias a todos. En la pregunta (3), las personas sintieron que la sintaxis debería ser clara (aunque recomendaría un comentario ocasional, especialmente si su código pasará a los desarrolladores que pueden no estar familiarizados con él).

En la pregunta (1), el código generado debería ejecutarse rápidamente. Los archivos .class extra causan el desorden en el archivo jar y ralentizan el inicio del programa (gracias a @coobird para medir eso). @Thilo señaló que la recolección de basura puede verse afectada, y el costo de la memoria para las clases cargadas adicionales puede ser un factor en algunos casos.

La pregunta (2) resultó ser lo más interesante para mí. Si entiendo las respuestas, lo que sucede en DBI es que la clase interna anónima amplía la clase del objeto que está construyendo el nuevo operador, y por lo tanto tiene un valor "este" que hace referencia a la instancia que se está construyendo. Muy aseado.

En general, DBI me parece algo así como una curiosidad intelectual. Coobird y otros señalan que puede lograr el mismo efecto con Arrays.asList, los métodos varargs, Google Collections y los literales de Java 7 Collection propuestos. Los lenguajes más nuevos de JVM como Scala, JRuby y Groovy también ofrecen notaciones concisas para la construcción de listas, e interoperan bien con Java. Dado que DBI satura la ruta de clases, ralentiza la carga de clases un poco y hace que el código sea un poco más oscuro, probablemente me rehúse. Sin embargo, planeo llevar esto a un amigo que acaba de obtener su SCJP y adora las buenas justas sobre la semántica de Java. ;-) ¡Gracias a todos!

7/2017: Baeldung tiene un buen resumen de la inicialización del doble refuerzo y lo considera un antipatrón.

12/2017: @Basil Bourque señala que en el nuevo Java 9 puedes decir:

Set<String> flavors = Set.of("vanilla", "strawberry", "chocolate", "butter pecan");

Eso es seguro el camino a seguir. Si está atrapado con una versión anterior, eche un vistazo a InmutableSet de Google Collections.


686
2018-05-29 03:40


origen


Respuestas:


Este es el problema cuando me dejo llevar por las clases internas anónimas:

2009/05/27  16:35             1,602 DemoApp2$1.class
2009/05/27  16:35             1,976 DemoApp2$10.class
2009/05/27  16:35             1,919 DemoApp2$11.class
2009/05/27  16:35             2,404 DemoApp2$12.class
2009/05/27  16:35             1,197 DemoApp2$13.class

/* snip */

2009/05/27  16:35             1,953 DemoApp2$30.class
2009/05/27  16:35             1,910 DemoApp2$31.class
2009/05/27  16:35             2,007 DemoApp2$32.class
2009/05/27  16:35               926 DemoApp2$33$1$1.class
2009/05/27  16:35             4,104 DemoApp2$33$1.class
2009/05/27  16:35             2,849 DemoApp2$33.class
2009/05/27  16:35               926 DemoApp2$34$1$1.class
2009/05/27  16:35             4,234 DemoApp2$34$1.class
2009/05/27  16:35             2,849 DemoApp2$34.class

/* snip */

2009/05/27  16:35               614 DemoApp2$40.class
2009/05/27  16:35             2,344 DemoApp2$5.class
2009/05/27  16:35             1,551 DemoApp2$6.class
2009/05/27  16:35             1,604 DemoApp2$7.class
2009/05/27  16:35             1,809 DemoApp2$8.class
2009/05/27  16:35             2,022 DemoApp2$9.class

Estas son todas las clases que se generaron cuando estaba haciendo una aplicación simple, y usé grandes cantidades de clases internas anónimas: cada clase se compilará en un archivo separado. class archivo.

La "inicialización de llaves dobles", como ya se mencionó, es una clase interna anónima con un bloque de inicialización de instancias, lo que significa que se crea una nueva clase para cada "inicialización", con el propósito de crear un solo objeto.

Teniendo en cuenta que la Máquina Virtual de Java necesitará leer todas esas clases cuando las use, eso puede llevar a un tiempo en el bytecode verfication proceso y tal. Por no mencionar el aumento en el espacio en disco necesario para almacenar todos aquellos class archivos.

Parece que hay un poco de sobrecarga al utilizar la inicialización de doble llave, por lo que probablemente no sea una buena idea ir demasiado al agua con ella. Pero como Eddie señaló en los comentarios, no es posible estar absolutamente seguro del impacto.


Solo como referencia, la inicialización con doble llave es la siguiente:

List<String> list = new ArrayList<String>() {{
    add("Hello");
    add("World!");
}};

Parece una característica "oculta" de Java, pero es solo una reescritura de:

List<String> list = new ArrayList<String>() {

    // Instance initialization block
    {
        add("Hello");
        add("World!");
    }
};

Entonces es básicamente un bloque de inicialización de la instancia eso es parte de un clase interna anónima.


Joshua Bloch Propuesta de Collection Literals para Project Coin estaba en la línea de:

List<Integer> intList = [1, 2, 3, 4];

Set<String> strSet = {"Apple", "Banana", "Cactus"};

Map<String, Integer> truthMap = { "answer" : 42 };

Lamentablemente, no hizo su camino ni en Java 7 ni en 8 y se archivó indefinidamente.


Experimentar

Aquí está el experimento simple que he probado: hacer 1000 ArrayLists con los elementos "Hello" y "World!" añadido a ellos a través de la add método, usando los dos métodos:

Método 1: Inicialización de doble abrazadera

List<String> l = new ArrayList<String>() {{
  add("Hello");
  add("World!");
}};

Método 2: crear una instancia ArrayList y add

List<String> l = new ArrayList<String>();
l.add("Hello");
l.add("World!");

Creé un programa simple para escribir un archivo fuente Java para realizar 1000 inicializaciones usando los dos métodos:

Prueba 1:

class Test1 {
  public static void main(String[] s) {
    long st = System.currentTimeMillis();

    List<String> l0 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    List<String> l1 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    /* snip */

    List<String> l999 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    System.out.println(System.currentTimeMillis() - st);
  }
}

Prueba 2:

class Test2 {
  public static void main(String[] s) {
    long st = System.currentTimeMillis();

    List<String> l0 = new ArrayList<String>();
    l0.add("Hello");
    l0.add("World!");

    List<String> l1 = new ArrayList<String>();
    l1.add("Hello");
    l1.add("World!");

    /* snip */

    List<String> l999 = new ArrayList<String>();
    l999.add("Hello");
    l999.add("World!");

    System.out.println(System.currentTimeMillis() - st);
  }
}

Tenga en cuenta que el tiempo transcurrido para inicializar el 1000 ArrayLists y las 1000 clases internas anónimas que se extienden ArrayList se verifica utilizando el System.currentTimeMillis, por lo que el temporizador no tiene una resolución muy alta. En mi sistema Windows, la resolución es de alrededor de 15-16 milisegundos.

Los resultados de 10 ejecuciones de las dos pruebas fueron los siguientes:

Test1 Times (ms)           Test2 Times (ms)
----------------           ----------------
           187                          0
           203                          0
           203                          0
           188                          0
           188                          0
           187                          0
           203                          0
           188                          0
           188                          0
           203                          0

Como se puede ver, la inicialización de la doble llave tiene un tiempo de ejecución notable de alrededor de 190 ms.

Mientras tanto, el ArrayList el tiempo de ejecución de inicialización salió a 0 ms. Por supuesto, la resolución del temporizador debe tenerse en cuenta, pero es probable que sea menor de 15 ms.

Entonces, parece haber una diferencia notable en el tiempo de ejecución de los dos métodos. Parece que de hecho hay algo de sobrecarga en los dos métodos de inicialización.

Y sí, hubo 1000 .class archivos generados al compilar el Test1 programa de prueba de inicialización de doble refuerzo.


529
2018-05-29 03:59



Una propiedad de este enfoque que no se ha señalado hasta ahora es que, debido a que crea clases internas, toda la clase que lo contiene se captura en su alcance. Esto significa que mientras su Set esté vivo, retendrá un puntero a la instancia que lo contiene (this$0) y evitar que se recolecte basura, lo que podría ser un problema.

Esto, y el hecho de que una nueva clase se crea en primer lugar a pesar de que un HashSet regular funcionaría bien (o incluso mejor), hace que no quiera usar este constructo (aunque realmente anhelo el azúcar sintáctico).

Segunda pregunta: el nuevo HashSet debe ser el "esto" utilizado en el inicializador de instancias ... ¿alguien puede arrojar luz sobre el mecanismo? Ingenuamente esperaba que "esto" se refiriera al objeto inicializando "sabores".

Así es como funcionan las clases internas. Ellos tienen su propio this, pero también tienen punteros a la instancia principal, de modo que también puede llamar a métodos en el objeto contenedor. En el caso de un conflicto de nombres, la clase interna (en su caso HashSet) tiene prioridad, pero puede prefijar "this" con un nombre de clase para obtener el método externo también.

public class Test {

    public void add(Object o) {
    }

    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // HashSet
              Test.this.add("hello"); // outer instance 
            }
        };
    }
}

Para tener claridad sobre la subclase anónima que se está creando, también podría definir métodos allí. Por ejemplo anular HashSet.add()

    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // not HashSet anymore ...
            }

            @Override
            boolean add(String s){

            }

        };
    }

89
2018-05-29 05:37



Tomando la siguiente clase de prueba:

public class Test {
  public void test() {
    Set<String> flavors = new HashSet<String>() {{
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }};
  }
}

y luego descompilando el archivo de clase, veo:

public class Test {
  public void test() {
    java.util.Set flavors = new HashSet() {

      final Test this$0;

      {
        this$0 = Test.this;
        super();
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
    };
  }
}

Esto no me parece terriblemente ineficiente. Si estuviera preocupado por el rendimiento de algo así, lo perfilaría. Y su pregunta 2 se responde con el código anterior: está dentro de un constructor implícito (e inicializador de instancia) para su clase interna, por lo que "this"se refiere a esta clase interna.

Sí, esta sintaxis es oscura, pero un comentario puede aclarar el uso de la sintaxis oscura. Para aclarar la sintaxis, la mayoría de la gente está familiarizada con un bloque de inicializador estático (inicializadores estáticos JLS 8.7):

public class Sample1 {
    private static final String someVar;
    static {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

También puede usar una sintaxis similar (sin la palabra "static") para el uso del constructor (Inicializadores de instancias JLS 8.6), aunque nunca he visto esto usado en el código de producción. Esto es mucho menos conocido.

public class Sample2 {
    private final String someVar;

    // This is an instance initializer
    {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

Si no tiene un constructor predeterminado, entonces el bloque de código entre { y } se convierte en un constructor por el compilador. Con esto en mente, desenrede el código de llave doble:

public void test() {
  Set<String> flavors = new HashSet<String>() {
      {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
  };
}

El bloque de código entre las llaves más internas se convierte en un constructor por el compilador. Los refuerzos externos delimitan la clase interna anónima. Para tomar esto como el paso final de hacer que todo no sea anónimo:

public void test() {
  Set<String> flavors = new MyHashSet();
}

class MyHashSet extends HashSet<String>() {
    public MyHashSet() {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }
}

Para propósitos de inicialización, diría que no hay ningún tipo de sobrecarga (o tan pequeño que pueda ser descuidado). Sin embargo, cada uso de flavors irá no en contra HashSet pero en cambio contra MyHashSet. Probablemente hay una pequeña (y posiblemente insignificante) sobrecarga para esto. Pero, nuevamente, antes de preocuparme por eso, lo describiría.

Una vez más, para su pregunta n. ° 2, el código anterior es el equivalente lógico y explícito de la inicialización del paréntesis doble, y lo hace obvio donde "this"se refiere a: a la clase interna que se extiende HashSet.

Si tiene preguntas sobre los detalles de los inicializadores de instancia, revise los detalles en JLS documentación.


34
2018-05-29 03:52



Cada vez que alguien usa la inicialización con doble llave, un gatito es asesinado.

Además de que la sintaxis es bastante inusual y no es realmente idiomática (el gusto es discutible, por supuesto), estás creando innecesariamente dos problemas importantes en tu aplicación, que acabo de bloguear recientemente con más detalle aquí.

1. Estás creando demasiadas clases anónimas

Cada vez que utiliza la inicialización con doble llave, se crea una nueva clase. P.ej. este ejemplo:

Map source = new HashMap(){{
    put("firstName", "John");
    put("lastName", "Smith");
    put("organizations", new HashMap(){{
        put("0", new HashMap(){{
            put("id", "1234");
        }});
        put("abc", new HashMap(){{
            put("id", "5678");
        }});
    }});
}};

... producirá estas clases:

Test$1$1$1.class
Test$1$1$2.class
Test$1$1.class
Test$1.class
Test.class

Eso es bastante caro para tu cargador de clases, ¡para nada! Por supuesto, no requerirá mucho tiempo de inicialización si lo hace una vez. Pero si lo hace 20,000 veces en su aplicación empresarial ... ¿todo ese montón de memoria solo por un poco de "azúcar sintáctica"?

2. ¡Estás creando una fuga de memoria!

Si toma el código anterior y devuelve ese mapa de un método, los llamantes de ese método podrían, sin sospecharlo, aferrarse a recursos muy pesados ​​que no pueden ser recolectados. Considere el siguiente ejemplo:

public class ReallyHeavyObject {

    // Just to illustrate...
    private int[] tonsOfValues;
    private Resource[] tonsOfResources;

    // This method almost does nothing
    public Map quickHarmlessMethod() {
        Map source = new HashMap(){{
            put("firstName", "John");
            put("lastName", "Smith");
            put("organizations", new HashMap(){{
                put("0", new HashMap(){{
                    put("id", "1234");
                }});
                put("abc", new HashMap(){{
                    put("id", "5678");
                }});
            }});
        }};

        return source;
    }
}

El retorno Map ahora contendrá una referencia a la instancia adjunta de ReallyHeavyObject. Probablemente no quieras arriesgarte a eso:

Memory Leak Right Here

Imagen de http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/

3. Puede pretender que Java tiene literales de mapa

Para responder a su pregunta real, las personas han estado usando esta sintaxis para pretender que Java tiene algo así como literales de mapas, similar a los literales de matrices existentes:

String[] array = { "John", "Doe" };
Map map = new HashMap() {{ put("John", "Doe"); }};

Algunas personas pueden encontrar esto sintácticamente estimulante.


33
2017-12-17 08:56



propenso a fugas

He decidido intervenir. El impacto en el rendimiento incluye: operación de disco + descomprimir (para jar), verificación de clase, espacio de perm-gen (para la JVM de Hotspot de Sun). Sin embargo, lo peor de todo es que es propenso a fugas. No puedes simplemente regresar.

Set<String> getFlavors(){
  return Collections.unmodifiableSet(flavors)
}

Entonces, si el conjunto escapa a cualquier otra parte cargada por un cargador de clases diferente y se mantiene una referencia allí, se filtró todo el árbol de clases + cargador de clases. Para evitar eso, se necesita una copia en HashMap, new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}}). No tan lindo más. No uso el modismo, yo mismo, en cambio es como new LinkedHashSet(Arrays.asList("xxx","YYY"));


32
2018-01-31 13:46



Cargando muchas clases puede agregar algunos milisegundos al inicio. Si el inicio no es tan crítico y observa la eficiencia de las clases después del inicio, no hay diferencia.

package vanilla.java.perfeg.doublebracket;

import java.util.*;

/**
 * @author plawrey
 */
public class DoubleBracketMain {
    public static void main(String... args) {
        final List<String> list1 = new ArrayList<String>() {
            {
                add("Hello");
                add("World");
                add("!!!");
            }
        };
        List<String> list2 = new ArrayList<String>(list1);
        Set<String> set1 = new LinkedHashSet<String>() {
            {
                addAll(list1);
            }
        };
        Set<String> set2 = new LinkedHashSet<String>();
        set2.addAll(list1);
        Map<Integer, String> map1 = new LinkedHashMap<Integer, String>() {
            {
                put(1, "one");
                put(2, "two");
                put(3, "three");
            }
        };
        Map<Integer, String> map2 = new LinkedHashMap<Integer, String>();
        map2.putAll(map1);

        for (int i = 0; i < 10; i++) {
            long dbTimes = timeComparison(list1, list1)
                    + timeComparison(set1, set1)
                    + timeComparison(map1.keySet(), map1.keySet())
                    + timeComparison(map1.values(), map1.values());
            long times = timeComparison(list2, list2)
                    + timeComparison(set2, set2)
                    + timeComparison(map2.keySet(), map2.keySet())
                    + timeComparison(map2.values(), map2.values());
            if (i > 0)
                System.out.printf("double braced collections took %,d ns and plain collections took %,d ns%n", dbTimes, times);
        }
    }

    public static long timeComparison(Collection a, Collection b) {
        long start = System.nanoTime();
        int runs = 10000000;
        for (int i = 0; i < runs; i++)
            compareCollections(a, b);
        long rate = (System.nanoTime() - start) / runs;
        return rate;
    }

    public static void compareCollections(Collection a, Collection b) {
        if (!a.equals(b) && a.hashCode() != b.hashCode() && !a.toString().equals(b.toString()))
            throw new AssertionError();
    }
}

huellas dactilares

double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 34 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns

18
2018-05-29 04:20



Para crear conjuntos, puede usar un método de fábrica varargs en lugar de la inicialización con doble llave:

public static Set<T> setOf(T ... elements) {
    return new HashSet<T>(Arrays.asList(elements));
}

La biblioteca de Google Collections tiene muchos métodos de conveniencia como este, así como muchas otras funcionalidades útiles.

En cuanto a la oscuridad del modismo, la encuentro y la uso en el código de producción todo el tiempo. Me preocuparía más que los programadores que se confunden con el idioma se les permita escribir código de producción.


16
2018-05-29 03:59